Aracne documentation
Aracne scans a source tree into a topology — a directed graph of every package, file, function, method, class, interface, named type, variable and dependency, plus the edges between them — and answers an agent's reads and searches from that graph instead of from raw text.
01Overview
The scan builds a topology and persists it to SQLite at .aracne/topology.db, next
to your code, then keeps it current as files change. A read returns the declaration you
asked for, framed by its signature and followed by a # CONTEXT: block naming
only the resources those lines actually mention, each with a one-line description. A search
also matches node names and stored descriptions, so a plain-English query
finds code that never says the word.
What’s in it
- The topology engine for Go, Python, JavaScript, TypeScript, Rust and Java — full, incremental and hard scans.
- Description generation as a sweep
(
arac descriptions generate) or lazily on the read path, against Anthropic, OpenAI or DeepSeek — or a CLI you are already logged into, with no API key. - Read, write and grep served from the topology — through the shell
your agent already types, through
arac read, or as MCP tools, depending on the mode. - Harness integration for Claude Code and OpenCode — contract, hooks,
plugins, sub-agents, slash commands and MCP config, all written by
arac setup.CLAUDE.md,AGENTS.mdand Aracne's own agent harness get one document, as long or as short ascontract_verbositysays. - The Viz menu — a local web visualizer with package, data-flow and custom views, neighborhood exploration, search and a source inspector. Full build
- Maintenance commands:
arac setup,arac warnings list,arac check-updates,arac scanner run,arac disable,arac descriptions export|import.
02Install & the two builds
Aracne publishes two builds of the same engine. They differ in exactly one thing: whether the web visualizer is compiled in.
| Build | Contains | Build command | Front-end |
|---|---|---|---|
| Full default | Engine, all six languages, every CLI surface, and the Viz web front-end | make build |
arac viz serve + embedded SPA |
| Basic | The same engine, the same six languages, the same full tool capability — no front-end. A smaller binary for CI and servers. | make build-basic |
none; arac viz serve exits with a pointer to the Full build |
Install a release binary
Release assets are named arac-<goos>-<goarch> for the Full build and
arac-<goos>-<goarch>-basic for Basic, with a
SHA256SUMS alongside them. Published for
linux/amd64, darwin/arm64 and windows/amd64;
the Windows pair carries .exe (arac-windows-amd64.exe,
arac-windows-amd64-basic.exe).
curl -LO https://github.com/Rhuan-Marques/aracne/releases/latest/download/arac-linux-amd64 chmod +x arac-linux-amd64 && sudo mv arac-linux-amd64 /usr/local/bin/arac arac --version
Install with Go
go install github.com/Rhuan-Marques/aracne/cmd/arac@latest
go install produces the Full build.
Build from source
git clone https://github.com/Rhuan-Marques/aracne && cd aracne make build # Full → bin/arac make build-basic # Basic → bin/arac-basic
03Quickstart
cd your-project arac init
That is the whole setup. arac init will guide you through some more questions to get your setup, when it finishes, it's all good to go
Warm the repository up. Run /descriptions-generate in your harness once
arac init finishes. It orchestrates from the main session, using your subscription
tokens to do the job.
04The topology
A topology is a set of resources (nodes) and connections
(typed, directed edges). Multiple languages merge into one graph; the topology's language is
then multi.
Resource kinds
| Kind | What it is | Notes |
|---|---|---|
package | A Go package | Python/JS/TS/Rust are modules-first and drop this node in favour of file-first IDs |
file | One source file | Carries the file's top-level declarations as containment edges |
function | A free function | |
method | A function bound to a type | Resolves as a function for read purposes |
struct | A struct, class or record | Python and JS/TS classes are stored as struct |
interface | An interface, trait or Protocol/ABC | |
named_type | A defined type, type alias or enum | |
variable | A package/module-level variable or constant | |
dependency | An external module the project imports | What makes the import x # external lines in a read possible |
Each resource carries an ID, that can be used to search for the resource. They also contain a Kind, a Name, a Language, a one-line Description, a Position
and their free-form Properties (typed input/output, for example) and its Connections.
Edge types
Connection keys are shared vocabulary across the scanners, so a graph walk needs no
per-language code. Go says uses_struct where Python says uses_class;
both mean "this resource references that type".
| Group | Edges |
|---|---|
| References (outgoing) | calls, uses_struct, uses_class, uses_interface, uses_named_type, uses_extvar, uses_package, uses_dependency |
| Containment | has_file, has_function, has_struct, has_class, has_interface, has_named_type, has_extvar, has_abstract_methods, methods, constructor |
| Type relationships | implements / implemented_by, inherits / inherited_by |
| Imports | imports, imports_package, imports_module, imports_dependency |
Scan modes
| Mode | Flag | What it does | Descriptions |
|---|---|---|---|
| incremental default | --default | Diffs the tree against file_manifest.json and re-parses only what changed | preserved |
| full | --all | Re-parses every file | preserved |
| hard | --hard | Rebuilds the database from scratch | cleared, along with bugs |
What the scanners skip
- Dot directories,
.git,.aracne,node_modules,vendor,__pycache__. - Test and build artifacts: Go
*_test.go, Pythontest_*.py, JS/TS*.test.*/*.spec.*/*.min.*. - Anything matched by
scan.ignore. - Anything hidden by
paths. - Files over
read.max_file_size.
The context block
A read returns source plus a # CONTEXT: section listing the neighbours those
exact lines touch, each with its description. Under
read.context_filter: "full" a
# USED BY: section lists callers as well.
Under read.context_filter: "normal" neighbours with no
description are omitted — which is what lazy descriptions fill in.
Constants are exempt: a value shown is a description.
05Resource IDs
Every declaration has a stable ID. IDs are the addressing scheme for
arac read, the read MCP tool, and — in the
intercept_id mode — for ordinary shell read commands, which accept an ID
wherever they accept a path.
| Language | Type / function | Method |
|---|---|---|
| Go | <module-path>/<pkg>.<Name> | <module-path>/<pkg>.(<Recv>).<Method> |
| Python | <pkg>.<module>.<Name> | <pkg>.<module>.<Class>.<method> |
| JavaScript | <dir>/<file>.<Name> | <dir>/<file>.<Class>.<method> |
| TypeScript | <dir>/<file>.<Name> | <dir>/<file>.<Class>.<method> |
| Rust | <crate>::<mod>::<Name> | <crate>::<mod>::<Type>::<method> |
| Java | <pkg>.<Class> (nested Outer.Inner) | <pkg>.<Owner>.<name>(<sig>) |
Java carries signatures because it overloads, and it mints synthetic names for the awkward
declaration forms: $anonN, $Local, Enum$CONST,
constructors as <Owner>.<init>(<sig>), initializer blocks as
<clinit>() / <instance-init>(), and record accessors as
<Rec>.<comp>().
IDs are forgiving. A unique trailing part is enough —
Flask.register_blueprint resolves as well as the full path. The resolver also
absorbs a wrong root prefix and the wrong separator convention, and on a miss returns
ranked candidates rather than an error.
Search output and # CONTEXT: entries print the ID of every declaration they
name.
06Descriptions
A description is one line of prose stored on a node in the topology database. It is the only
thing grep can match that does not exist in the source text at all — so a
plain-English query finds a node whose code never says the word.
Descriptions are generated three ways:
Comments
Any resources that already have comments on how they work are automatically converted, as long as they follow their character budgets.
The sweep
arac descriptions generate lists the undocumented resources of the targeted
kinds, splits them into deterministic non-overlapping batches, and fans out
descriptions-generation-executor sub-agents — one per batch, all in one
concurrent wave. When the wave finishes it re-lists what is still undocumented (the database
is the source of truth, not the executors' self-reports) and launches another wave, until
none remain.
Kinds default to function, method, struct,
interface and are set by
descriptions.kinds or the
--targets flag.
Lazy descriptions on by default
descriptions.lazy generates a missing description at the moment a read
or a search is about to show it, rather than only in a sweep.
Who writes them
The sweep and the lazy fill read one provider block, on the
descriptions section itself.
arac init asks for it — an API key, or a CLI
you are already logged into — or you can write the block by hand.
| Key | What it does |
|---|---|
provider | anthropic, openai, deepseek, or cli to run a command. Absent means unanswered, and arac init asks. |
base_url | A gateway or proxy for the API providers. Ignored by the CLI ones. |
api_key_env | The environment variable the API key is read from. Absent falls back to the provider's own name. Ignored by cli. |
cli_provider_command | The command provider: "cli" runs. Required by it, ignored by everything else. |
The model is set elsewhere — on the
descriptions-generation-executor agent, which both the sweep and the lazy fill
describe with:
{ "llm": { "<any>": { "agents": {
"descriptions-generation-executor": { "model": "claude-haiku-4-5" }
} } } }arac init asks for it and writes it under <any>, so one
answer covers Claude Code, OpenCode and the lazy fill alike. It offers the chosen provider's
cheap tier — claude-haiku-4-5, gpt-5.4-mini or
deepseek-v4-flash — and leaving it unset is fine: a cheap tier is used
anyway.
Under a CLI provider the model lives in the command. The CLI transport has no model
parameter of its own, so name the model in the command itself —
claude -p --model sonnet.
The three API providers read ANTHROPIC_API_KEY, OPENAI_API_KEY and
DEEPSEEK_API_KEY unless api_key_env names another variable. Nothing
else in Aracne needs a key.
Describing with the Claude CLI, no API key
provider: "cli" describes through a CLI you are already logged into, spending
that subscription instead of an API key: Aracne runs a command, writes the batch to its
stdin, and reads the descriptions off its stdout.
"descriptions": { "provider": "cli", "cli_provider_command": "claude -p" }
That is the whole setup. claude must be on PATH and already logged
in — running it once, interactively, is enough. Both entry points then use it:
arac descriptions generate # the sweep, through `claude -p` arac read internal/server.go # a lazy fill, through the same command
Pin a model and cap the turns — the batch already carries every resource's source, so there is nothing for the CLI to go and fetch:
"cli_provider_command": "claude -p --model haiku --max-turns 1"
It does not have to be Claude. Any command that answers a prompt on stdout will do, such as
"cli_provider_command": "codex exec".
What to know before switching.
- It is argv, not a shell line. Words split on whitespace,
'and"group,\escapes. No pipes, no redirection, no$VAR. - It spends the subscription behind that CLI — the same quota you are typing into — and adds a process launch per batch.
- The command must answer in one shot. Its stdin carries the
instructions and the batch; its stdout is parsed as
<resource id> :: <description>lines, and anything else is ignored. A CLI that needs a flag to be non-interactive needs that flag here.
One run, without touching the config
arac descriptions generate --cli describes the repository through a command for
that run only, overriding whatever descriptions.provider says.
arac descriptions generate --cli "claude -p" # a command you named arac descriptions generate --cli "codex exec" # any command works here too arac descriptions generate --cli # the default, `claude -p` — asks first
Quote a command that has flags of its own: bare --cli and
--cli <command> are the same flag, so --cli claude -p would read
-p as one of Aracne's flags.
A bare --cli stops and asks before it spends anything. It runs
claude -p, which spends that subscription rather than an API key, and a sweep can
be thousands of resources at one process launch per batch. Aracne waits for a y.
Naming the command (--cli "claude -p") or passing -y skips the
question. With no terminal to ask — a pipe, a cron job, CI — a bare
--cli refuses rather than assuming an answer.
Character budgets
Descriptions have a per-kind budget, enforced on write: 120 characters for
a function or method, 100 for a type, 80 for a variable. Rows
written before the budget existed are grandfathered;
arac descriptions clear --oversized is the explicit sweep for those, and
arac descriptions generate --regen_oversized rewrites them instead of describing
undocumented nodes.
Getting them out of the database
arac descriptions exportbacks them up to an ID-independent JSONL sidecar (.aracne/descriptions.jsonl), andimportrestores them after a re-scan. Do this before a--hardscan, or before any change that re-mints IDs.
07Topology warnings
After an edit, Aracne can tell an agent that its change may have broken callers. Warnings
are surfaced through arac warnings list and the
warnings_list tool, and the post-call drift check reports
the ones a write causes.
| Kind | Means |
|---|---|
use_missing_node | A resource references something that is not in the graph. |
node_removed | A node that others referenced is gone. |
signature_changed | A callee's signature changed and a recorded call site still fails to fit it. Names the callee (SourceID) and the caller to verify (TargetID). |
interface_conflict | A type declares an interface and does not deliver it. |
The last two are derived from current state, not remembered from an event, which is what lets them retire themselves: fix the caller and the warning disappears on the next scan.
interface_conflict is only raised where declaring an interface is a claim that can
be wrong: Rust, Java, TypeScript, Python ABC. Go and Python Protocol are
structural, so a type that does not satisfy an interface simply has no
implements edge.
08The four modes
mode in .aracne/config.json is the one dial. It
answers three questions at once — which tools exist, which shell commands Aracne
answers, and what vocabulary the generated contract teaches — and every artifact
arac setup writes derives from it.
It is question two of arac init, which shows each mode with
what it means and an example of what it looks like. Editing the key by hand and re-running
arac setup is the other way; arac setup only ever reads it.
It decides which capabilities the contract may name;
contract_verbosity decides how much it says about
them. See The injected contract.
| mode | MCP tools | shell reads | shell grep | blocked_tools | addressing |
|---|---|---|---|---|---|
cli default |
none | run as themselves | intercepted | active | resource IDs, via arac read |
intercept_id |
none | intercepted | intercepted | inert | resource IDs, as command operands |
intercept_line_ranges |
none | intercepted | intercepted | inert | path:start-end |
mcp |
one read (+ warnings_list) | run as themselves | intercepted | active | resource IDs |
Three things hold in every mode. Shell grep is answered by the
annotated grep. Edits re-sync the topology. And arac read / arac grep
work from the CLI regardless.
Which one to pick
- Start with the default.
clicosts nothing per request and gives you the annotated grep, the edit sync and the contract. - Use
intercept_line_rangesif you want reads answered from the graph without teaching the model a new addressing scheme: every declaration is named by the exact lines it spans (src/parser.rs:940-1080), and reading an advertised span returns byte-for-byte what the resource read would have. - Use
intercept_idif you want IDs specifically — an ID cannot land mid-declaration and does not go stale when the file shifts. - Use
mcpon a harness where a real tool schema is worth more than a shell rewrite, or where you wantblocked_toolsto be able to refuse a native read.
grep, edit and write are not MCP tools in any
mode — their shell forms are intercepted everywhere.
09The terminal surface
In the intercepting modes, the shell is the tool surface. The agent writes
head -40 server.go and reads real stdout; a PreToolUse hook rewrites
the call into arac cmd -- head -40 server.go, which answers it from the topology:
the exact lines asked for, framed by the signature of whatever declaration they sit inside
(with an elision marker for what was left out), plus a # CONTEXT: block
restricted to the resources those lines actually mention.
A resource ID stands wherever a path does, so head -20 app.Flask is the first
twenty lines of the class body.
The agent never types arac cmd itself. The guard hook rewrites its
Bash call into it. You can type it yourself to see what an agent would get.
What is modelled
| Command | Window |
|---|---|
cat, less, more | whole file |
bat | whole file, or --line-range |
head -N | first N lines |
tail -N, tail -n +N | last N lines / from N |
sed -n 'A,Bp' | an explicit range |
awk 'NR>=A && NR<=B' | an explicit range |
git show, git cat-file | whole file at a revision |
Get-Content (PowerShell) | whole file, -TotalCount, -Tail |
grep, egrep, fgrep, rg, ack, ag, ug | search — answered in every mode |
What passes through, byte-identically
Anything Aracne does not model runs exactly as it would have, and the real binary's exit status is preserved.
- Any flag not modelled —
head -c,tail -f,grep -o,sedsubstitutions. - Readers that transform rather than window —
nl,tac,xxd,od,hexdump,strings. - An unindexed file, a missing database, or a file with no topology nodes.
- An over-budget answer — a read or a search past
terminal.max_overservetimes the bytes the real command would have printed. - Anything the guard will not rewrite — across a pipe, a redirect, a
heredoc, an
&&, an env prefix or a wrapper; a mutation; or a command already rewritten once.
A search that found nothing exits 1, the way grep and
rg do.
The guard, mode by mode
| Behaviour | cli | intercept_id | intercept_line_ranges | mcp |
|---|---|---|---|---|
| Rewrites shell search | yes | yes | yes | yes |
| Rewrites shell reads | no | yes | yes | no |
blocked_tools can deny | yes | no | no | yes |
| Post-call nudge on a shell read | yes | no | no | no |
| Pre-call freshness scan | yes | yes | yes | yes |
Where blocking applies, interception is tried first: a command Aracne can
serve is answered rather than refused, however blocked_tools reads.
blocked_tools is empty in a generated config and ships
warn-only. Denial is not needed for graph consistency — the edit-sync
hook re-syncs the topology after a native edit.
Freshness
Before every tool call the guard sees, it runs the
scan.pre_tool scan, so the call is answered from a
graph that matches the code on disk — including changes nothing in the session made,
like a git checkout, a rebase or an editor save. Claude Code gets this from the
PreToolUse guard hook; OpenCode gets it from
arac-pre-tool-scan.js, installed unconditionally. Both read the same config key
at call time, so scan.pre_tool: "none" disables it on both surfaces without
re-running init.
The pre-call scan reports nothing. The warnings it finds are persisted and surface through
warnings_list / arac warnings list.
10The MCP server
arac serve is a JSON-RPC 2.0 MCP server over stdio, supporting
initialize, tools/list and tools/call. It is how
Claude Code and OpenCode consume Aracne in mcp mode, configured in
.mcp.json / opencode.json.
No API key is needed. The host platform brings its own model; the server only answers questions from the graph.
{
"mcpServers": {
"aracne": {
"command": "arac",
"args": ["serve", "--tool-profile", "main", "--harness", "claude_code"]
}
}
}Tool profiles
--tool-profile and --harness together select which config-resolved
tool set is exposed. A profile is an agent name: the server looks up
llm.<harness>.agents.<name>.mcp_tools, falling back through
llm.<any> and then the main agent.
- --tool-profile main
- The main agent's tools. What Claude Code is wired to.
- --tool-profile all
- Every servable tool. What OpenCode is wired to, because its permission block does the narrowing instead.
- --tool-profile descriptions-generation-executor
- Just
read+update_description. - --harness claude_code
- Default. Applies the
claude_codeoverrides. - --harness opencode
- Applies the
opencodeoverrides.
Whatever a profile names, grep, edit and write are
always dropped (they are shell-served in every mode), and nothing at all is served outside
mcp mode.
Sub-agents keep their MCP server even on the terminal surface. mode
governs the main agent's surface only. Generated sub-agents declare their own scoped
server inline in their frontmatter (mcpServers: →
arac serve --tool-profile <agent>), because they exist to call
update_description, which has no shell equivalent.
11CLI reference
The binary is arac. Running it with no arguments prints the usage banner. The
viz serve block is absent from the Basic build.
arac --version
Print the version this binary was stamped with at link time. Aliases:
-v, version. An un-stamped build reports dev.
Scanning
arac scan [flags]
Build or update the topology. With no flags this is an incremental scan: it diffs the tree against the manifest and re-parses only what changed, so the usual case is fast and the usual case of nothing having changed is a no-op.
- -root <path>
- Root folder of the project. Default
. - -output <file>
- Output SQLite database path. Default
<root>/.aracne/topology.db - --all
- Re-scan every file. Descriptions are preserved.
- --hard
- Force a full rebuild from scratch. Descriptions and bugs are cleared. Export descriptions first.
- --default
- Force the incremental scan. This is what a bare
arac scanalready does. - --debug
- Compare warnings before and after the scan and print the differences.
- --verbose, -v
- List the changed files an incremental scan detected.
- --workers <n>
- Max files parsed concurrently during a full scan. 0 = auto, one per CPU. Lower it to cap peak RAM on large projects.
- --progress <m>
- Progress bar:
auto(on a terminal, above 15 files),always, ornever. Defaultauto
arac scan arac scan --all --verbose arac scan -root ./services/api -output ./services/api/.aracne/topology.db
arac scanner run [--db <path>]
Watch source files and run incremental scans as they change, in the foreground. This
paired with scan.pre_tool: "none" in the config files can make your LLM work
faster by removing the need to scan before tool-calls. It also allows for updated reads in
single-tool pipelines that both edit and read in succession.
Polling interval comes from scanner.update_frequency (milliseconds).
Reading & searching
arac read [--kind <kind>] [--full] <resource-id>…
Read one or more resources by ID and print their source with connected context. Results
are grouped by declaring file and share a single # CONTEXT:
section, so one batched call costs far less than one call per ID. Past
terminal.max_overserve the source goes out without
that section.
- --kind <kind>
- Force an exact kind:
function,method,struct,named_type,interface,variable,file,package,dependency. Also accepts--kind=<kind>. - --full
- Return whole file bodies verbatim even under
read.file_mode: "skeleton". No effect on non-file IDs.
arac read internal/topology/golang.GoManager arac read internal/cli/read.go arac read internal/cli.RunRead internal/cli.parseReadArgs arac read --kind interface Shape
There is no --lines. Use sed -n '10,40p' file for a
line window.
read.kinds gates this surface like every other. An
ID that resolves to a kind the project does not allow is refused by name, and a read in
which nothing resolved — a typo, a missing file, or a disallowed kind
— prints its report to stderr and exits 1.
arac grep [flags] <pattern> [path]
Regex search over node names, node descriptions and file contents, ranked
in that order. Returns path:line:match, naming the enclosing node above its
matches — which often answers the question with no follow-up read. Honours ripgrep's
ignore system; any files outside Aracne's topology (unsupported languages, text files,
scan.ignore files, etc) are shown like a usual grep call.
Past terminal.max_overserve the
matches go out without their headers and node rows.
- --db <path>
- Topology database. Default
.aracne/topology.db - --glob <pattern>
- Filename glob, e.g.
'*.go'or'**/*_test.ts'. - --type <lang>
- Language shorthand:
go,py,js,ts,rust,java,c,cpp,md,json,yaml,toml,sh. - -i
- Case-insensitive match.
- --output-mode <m>
content(default),files_with_matches, orcount.- --head-limit <n>
- Max matching lines. Default 200;
-1for no limit. - -B <n>
- Lines of context before each match.
- -A <n>
- Lines of context after each match.
- -C <n>
- Context on both sides. Overrides
-A/-B.
Which kinds may match on their description is
grep.description_kinds — by default
function, method, struct, interface. An
empty list disables description matching entirely.
arac grep "retry" arac grep --type go --glob 'internal/**' "WithFileLock" arac grep -i --output-mode files_with_matches "deprecated"
arac resource list [query] [--kind <kind>]… [--no-description]
List resources, optionally filtered by a name query, by kind (repeatable), or narrowed to
those with no description. The fallback the /descriptions-generate command uses
when the node_list_no_description tool is not available.
arac resource list --kind interface arac resource list Handler --kind struct --kind interface arac resource list --no-description
arac node count [--no-description]
Print the total node count, or with --no-description the number of
undocumented nodes. The one-line answer to "how warm is this repo?".
The terminal surface
arac cmd -- <command> [args…]
Run a shell read or search, answered from the topology when Aracne can and by the real binary when it cannot. See The terminal surface for exactly what is modelled and what passes through.
Agents do not type this — the guard hook rewrites their Bash call into it. You can type it yourself to see what an agent would get.
arac cmd -- head -40 internal/cli/guard.go arac cmd -- cat internal/cli.RunGuard # an ID stands where a path does arac cmd -- grep -rn "retry" internal/ arac cmd -- xxd logo.png # passthrough, byte-identical
A refusal is not a passthrough. When Aracne has nothing to add, the command is
handed back to the shell. When read.kinds forbids
the read, arac cmd prints to stderr and exits 1 instead.
Descriptions
arac descriptions generate [flags]
Generate descriptions for undocumented resources by fanning out executor sub-agents.
It reads its provider from the config and asks nothing. A
project that has not named one is told to run arac init, given the keys to write
by hand, and the command exits non-zero.
- --targets <kinds>
- Comma-separated resource kinds, overriding
descriptions.kinds. Defaultfunction,method,struct,interface - --batch-size <n>
- Max resources per executor. Default 5
- --parallel <n>
- Max executors running concurrently. Default 4
- --max-retries <n>
- Max executor attempts per resource. Default 3
- --progress <mode>
- Progress bar:
auto(on a terminal when there is anything to describe),always, ornever. Defaultauto - --include-not-visible
- Include resources
read.context_filterwould not render as a normal line — small functions and external variables under thefullor hidden settings. Mirrorsdescriptions.include_not_visiblefor one run. - --regen_oversized
- Rewrite existing descriptions that overrun their kind's character budget (function/method 120, type 100, variable 80) instead of describing undocumented resources.
- --cli [command]
- Describe by running a command instead of calling an API with a key, overriding
descriptions.providerfor this run. Quote a command with flags of its own:--cli "codex exec". Bare--clirunsclaude -pand asks first. - -y
- Auto-confirm the bare
--cliprompt.
arac descriptions clear [--target <kinds>] [--oversized]
Delete stored descriptions.
- --target <kinds>
- Comma-separated kinds to clear. Omit to clear every description.
- --oversized
- Clear only descriptions that overrun their kind's character budget, leaving the rest untouched.
arac descriptions export [--out <path>] [--db <path>]
Back up descriptions to an ID-independent JSONL sidecar. Run this before a
--hard scan or anything else that re-mints IDs.
- --out <path>
- Default
.aracne/descriptions.jsonl - --db <path>
- Default
.aracne/topology.db
arac descriptions import [flags]
Restore descriptions from a sidecar after a re-scan.
- --in <path>
- Sidecar to restore. Default
.aracne/descriptions.jsonl - --db <path>
- Default
.aracne/topology.db - --dry-run
- Report what would be restored without writing.
- --min-rate <0..1>
- Exit
2when the match rate falls below this. Use it in CI to catch a sidecar that no longer fits the graph. - --report <path>
- Write unmatched records to this JSONL path.
arac descriptions export arac scan --hard arac descriptions import --min-rate 0.9 --report /tmp/unmatched.jsonl
arac update-description <id> <kind> <description>
Set one resource's description directly. The CLI form of the
update_description tool.
Mutating code
arac edit < stdin JSON
Apply exact string replacements and re-sync the topology inline. Takes JSON on stdin, in either of two shapes.
Single edit. old_string must match exactly once unless
replace_all is true. An empty new_string deletes
the matched text.
echo '{"file_path":"main.go","old_string":"foo","new_string":"bar"}' | arac editBatch. Preferred: the edits apply in order, may span as many files as you like, take one file lock per file, and nothing is written unless all of them match — so a batch is safer than a sequence of single edits as well as far cheaper.
echo '{"edits":[
{"file_path":"a.go","old_string":"x","new_string":"y"},
{"file_path":"b.go","old_string":"p","new_string":"q"}
]}' | arac editarac write < stdin JSON
Create or overwrite a file, creating parent directories, and update the topology inline.
echo '{"file_path":"internal/new.go","content":"package internal\n"}' | arac writearac update-file <path> [--db <path>]
Re-parse one file into the topology and print any warnings the change produced. This is
what the edit-sync hook runs after a native edit, so the graph stays current
even when the change bypassed arac edit / arac write.
arac update-file --claude-hook is the hook entry point; it reads a Claude Code
hook event on stdin instead of taking a path.
Index health & analysis
arac warnings list [flags]
List outstanding topology warnings. See Topology warnings for what each kind means.
- --db <path>
- Default
.aracne/topology.db - --source <id>
- Filter by source resource ID.
- --target <id>
- Filter by target resource ID.
- --kind <kind>
use_missing_node,node_removed,signature_changed,interface_conflict.
arac check-updates [--db <path>] [--root <path>] [--json]
Index health: which files drifted from the topology since the last scan. Reports added,
modified and deleted paths. Exits 1 when the index is stale, so
it drops straight into CI or a pre-commit hook.
arac check-updates --json
# {"db":"...","root":"...","stale":true,"drifted":3,"added":[...],"modified":[...],"deleted":[...]}Integration & servers
arac init [--global]
Set a project up, start to finish. A full-screen, arrow-key flow that asks
the setup questions, saves them to
.aracne/config.json, scans the project, writes the integration, and — if
that is what you answered — describes the repository. Esc or Ctrl-C
cancels, and a cancel writes nothing at all: not the config, not the
.aracne directory.
- --global
- Install the integration at user level (
~/.claude/,~/.config/opencode/) so it applies across all projects.
It needs a terminal. In a pipe, a cron job or CI it exits non-zero and names
arac setup and the config keys it would have set, rather than hanging on a prompt
nobody is going to answer.
arac setup [flags]
Write the integration files from the config, asking nothing. With no harness flag it
writes both Claude Code and OpenCode. This is what to re-run after editing
.aracne/config.json by hand. Fully idempotent: re-running replaces Aracne's own
blocks and leaves everything else in those files alone. It validates the config and exits
non-zero on an invalid one.
- --claude
- Claude Code integration only.
- --opencode
- OpenCode integration only.
- --global
- Install at user level (
~/.claude/,~/.config/opencode/) so it applies across all projects. - -y
- Auto-confirm every replacement prompt.
It reads mode and never writes it, and announces which of
the four modes the project is in. See Harness integration for the full
list of files.
arac disable [flags]
Remove the integration: the MCP server entry, the permission block, the generated
commands and agents, the hooks and plugins, and the contract block from
CLAUDE.md / AGENTS.md. It does not touch
.aracne/ — your topology and config survive.
- --claude
- Claude Code only.
- --opencode
- OpenCode only.
- --all
- Both.
- --global
- Disable the user-level install.
- -y
- Auto-confirm every prompt.
arac serve [--tool-profile <agent>] [--harness <name>]
Start the MCP server on stdio. See The MCP server.
- --tool-profile <agent>
main,all, or a configured agent name such asdescriptions-generation-executor.- --harness <name>
claude_code(default) oropencode— whose per-agent overrides apply.
arac guard --claude-hook | --pre-scan
The hook entry points. You do not run these by hand.
- --claude-hook
- Reads a Claude Code hook event on stdin and emits the hook response: the pre-call freshness scan, the interception rewrite, a
blocked_toolsdenial where one applies, and the post-call nudge. Any read or parse error fails open. - --pre-scan
- Runs the configured
scan.pre_toolscan against the project. What OpenCode'sarac-pre-tool-scan.jsplugin calls. Prints nothing and always exits 0.
arac viz serve [--db <path>] [--addr <addr>] Full build
Start the local topology visualizer. See The Viz menu.
- --db <path>
- Default
.aracne/topology.db - --addr <addr>
- HTTP listen address. Default
127.0.0.1:7331
On the Basic build this exits 1 with a pointer to the Full build, rather than
pretending viz is not a command.
12MCP tool reference
Every tool name that can be referenced in .aracne/config.json is validated at load
and init time, so a typo fails fast rather than silently disabling a tool.
| Tool | MCP | What it does |
|---|---|---|
read | yes | Read any resources by ID — one call takes several, and returns their source plus connected context |
warnings_list | yes | List topology warnings |
update_description | yes | Set a resource's description |
node_list_no_description | yes | List undocumented resources needing descriptions |
grep | shell | Search node names, descriptions and contents. Not registered as an MCP tool in any mode — the shell form is intercepted everywhere |
edit | shell | Exact string replacements, batchable, topology synced inline. Shell-served |
write | shell | Create or overwrite a file, topology synced inline. Shell-served |
Default tool sets
| Agent | Default mcp_tools |
|---|---|
| main | read, warnings_list |
descriptions-generation-executor | read, update_description |
The tools in detail
read also registers as read_resource
Read one or more resources by ID and get their source plus the context they connect to.
Results are grouped by file and share a single context section — the
same output as arac read, and the same
terminal.max_overserve ceiling over it.
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | array of string | yes | Resource IDs to read. Duplicates ignored. A bare string is also accepted, because models emit a scalar often enough that rejecting it would just buy a correction turn. |
full | boolean | no | Return whole file bodies verbatim instead of signatures with large bodies elided. Only advertised when file reads are abridged. |
There is no start_line/end_line. Read the
declaration, not a window into it.
Why two names. The tool registers as read_resource when the harness
keeps its own native read, and as plain read when blocked_tools
denies that.
Which kinds are readable is read.kinds; this tool
only decides whether an agent may read at all.
grep shell-served
Regex search over topology node names, node descriptions and file contents, ranked in that
order. Descriptions are searchable only here. Returns path:line:match, naming the
enclosing node above its matches. Results are capped; the reply says what was withheld. Past
terminal.max_overserve the matches go out without
their headers and node rows.
| Parameter | Type | Required | Description |
|---|---|---|---|
pattern | string | yes | Regex to match |
path | string | no | File or directory to search. Default . |
glob | string | no | Filename glob, e.g. *.go, **/*_test.ts |
type | string | no | Language filter: go, py, js, ts, rust, java, c, cpp, md, json, yaml, toml, sh |
case_insensitive | boolean | no | Match case-insensitively |
output_mode | string | no | content (default), files_with_matches, count |
head_limit | integer | no | Max matching lines. Default 200; -1 unlimited |
before / after | integer | no | Context lines around each match |
edit shell-served
Replace exact text in one or more files, or delete it by passing an empty
new_string. old_string must match exactly once unless
replace_all is set. The topology is updated automatically.
| Parameter | Type | Description |
|---|---|---|
edits | array of object | A list of {file_path, old_string, new_string, replace_all} applied in order. May span several files. Preferred: one call, one topology sync per file, all-or-nothing. |
file_path | string | Single-edit form |
old_string | string | Exact text to find |
new_string | string | Replacement. Empty string deletes the matched text |
replace_all | boolean | Replace every occurrence instead of requiring a unique match. Default false |
An omitted new_string is an error, not a silent deletion — pass
"" explicitly to delete.
write shell-served
Write a file, creating parent directories and overwriting if present. The topology is updated automatically.
| Parameter | Type | Required |
|---|---|---|
file_path | string | yes |
content | string | yes |
warnings_list
List outstanding topology warnings: missing references, removed resources, changed signatures, interface conflicts. Grouped by kind, with a summary and details.
| Parameter | Type | Description |
|---|---|---|
source_id | string | Filter by source resource ID |
target_id | string | Filter by target resource ID |
kind | string | use_missing_node | node_removed | signature_changed | interface_conflict |
update_description
Set a topology resource's description. Takes id, resource_name
and description.
node_list_no_description
List the undocumented resources of the configured kinds. No parameters. What the
/descriptions-generate orchestration prefers over
arac resource list --no-description.
13Harness integration
Two commands, split along one line: arac init asks, and
arac setup writes. The wizard collects the answers, saves them,
scans, and then runs the writer itself; every later re-render is arac setup
alone.
Every artifact is generated from the mode key, with
contract_verbosity deciding how much the contract
says.
Setting a project up
arac init takes over the terminal and asks up to seven questions, one per screen,
each option carrying an explanation and a worked example that redraws as the highlight moves.
Arrow keys move, Enter selects, Esc cancels.
▄▀█ █▀█ ▄▀█ █▀▀ █▄░█ █▀▀
█▀█ █▀▄ █▀█ █▄▄ █░▀█ ██▄
How should the model reach your code? 2/7
This is the one dial. It decides which tools exist, which shell commands
aracne answers, and what the generated contract teaches. Shell `grep` is
answered by aracne and edits re-sync the topology in all four.
> cli No MCP tools. The contract points at `arac read
mcp <id>`.
intercept_id
intercept_line_ranges Shell reads run as themselves; the model is taught to
prefer resource IDs and reaches them through the
terminal.
The default, and the cheapest: nothing is added to
the tool schema of every request.
$ arac read internal/cli.RunSetup
internal/cli.RunSetup (function)
↑/↓ move ⏎ select esc cancel| # | Question | Writes |
|---|---|---|
| 1 | Which harness — Claude Code, OpenCode, or both | nothing; it picks which tree below gets written |
| 2 | How the model reaches your code | mode |
| 3 | Who writes the descriptions — an API key (then the wire format and the key's variable) or a CLI command | descriptions.provider + api_key_env or cli_provider_command |
| 4 | Which model writes them | llm.<any>.agents.descriptions-generation-executor.model |
| 5 | Describe the repository now, or lazily as you read | nothing — it decides whether this run sweeps |
| 6 | How much the contract says | contract_verbosity |
| 7 | Keep Aracne's tools loaded in context — asked only on Claude Code with mode: "mcp", the one combination it means anything on | preload_mcp_tools |
Every open question — the key's variable, the command, the model — offers a
default on the first row and Other: on the second, where you type.
Question 5 saves nothing. Lazy generation is on either way — the question only decides whether this run also sweeps the repository. Either answer is available at any size.
Claude Code
| File | Contents | When |
|---|---|---|
CLAUDE.md | The # Aracne contract block for the project's mode and verbosity — the same document AGENTS.md and Aracne's own agent harness get | always |
.mcp.json | mcpServers.aracne → arac serve --tool-profile main --harness claude_code | mcp mode only — and removed when the mode moves away from mcp |
.claude/settings.json | PreToolUse + PostToolUse guard hook registrations, matcher Read|Grep|Edit|Write|Bash; the edit-sync PostToolUse entry when the plugin is listed; MCP tool pre-approvals in mcp mode; env.ENABLE_TOOL_SEARCH = "false" if preload_mcp_tools is on | always (merged, preserving your own hooks) |
.claude/hooks/arac-guard.sh (.ps1 on Windows) | → arac guard --claude-hook | always |
.claude/hooks/arac-update-file.sh | → arac update-file --claude-hook | only with the edit-update-db-plugin plugin |
.claude/commands/*.md | Slash commands | always (bug ones gated) |
.claude/agents/*.md | Sub-agents, each with its own inline mcpServers: | always (bug ones gated) |
OpenCode
| File | Contents | When |
|---|---|---|
AGENTS.md | The # Aracne contract block, byte-identical to the one written into CLAUDE.md | always |
.opencode/opencode.json | mcp.aracne → arac serve --tool-profile all --harness opencode, plus a permission block: aracne_*: deny with each servable tool explicitly allowed, and native read/edit/bash permissions derived from blocked_tools | MCP entry in mcp mode; permissions always |
.opencode/plugins/arac-pre-tool-scan.js | tool.execute.before → arac guard --pre-scan | always, unconditionally |
.opencode/plugins/arac-native-edit-sync.js | Topology sync after a native edit | only with the edit-update-db-plugin plugin |
.opencode/commands/*.md | Slash commands | always (bug ones gated) |
.opencode/agents/*.md | Sub-agents | always (bug ones gated) |
The injected contract
arac setup writes a block into CLAUDE.md / AGENTS.md
explaining to the model how to use it. The contract is different per-mode.
contract_verbosity — how much it says
Two prompt lengths are available per mode, set by
contract_verbosity.
low
Meant to be used for high-end models, short explanation to save tokens, as these models can abstract Aracnes functionality in action
high
Usefull for lower-end models, very clear explanations and instruction with examples and behavioural rules.
Only high varies by language: how an ID is spelled, what a read returns, and
how the graph models that language.
# Aracne
`.aracne/topology.db` holds a pre-analyzed graph of this repo: every function,
type, interface and variable, where it is declared, a one-line description, and
what it references.
`arac read <id> <id> ...` returns declarations with their source, their imports
and a `# CONTEXT:` list of what they touch AND what implements, subclasses or
uses them, each with its description. Several ids in one call.
**Reach for it the moment you want to know** where something is defined, who
calls or implements it, or what one declaration does inside a large file. Each
of those is `arac read <name>` -- not `grep -rn`, not `cat`, not `sed -n`, not
`find`. Open a whole file only for a config, an unsupported language, or when
you genuinely need all of it.
**The bare name is enough** -- any unique trailing part of an id resolves, and a
miss returns the nearest candidates, so guess rather than searching for one
first. To learn about the function `NotifyPlayers`, run
`arac read NotifyPlayers`.
A CONTEXT entry's description is usually already the answer; drill in only to
change or deeply understand that neighbour.
Edits re-sync the graph; act on any topology warning that comes back.
Parallelise multiple reads and edits in a single command when possible.With blocked_tools set, a ## Tool Guard section is appended
before that closing line, naming which calls the project denies and that the refusal carries
the answer or names the tool that has it.
# Aracne `.aracne/topology.db` holds a pre-analyzed graph of this Go project: packages, files, functions, methods, structs, interfaces and package-level variables, with a one-line description on each and the references between them. It is the source of truth about this codebase, and it re-syncs after every edit. ## How it reaches you `arac read <id> <id> ...` returns declarations with their source, the imports they need, and the context around them. It takes a LIST: pass every ID you already know you need in ONE call. [...] ## Resource IDs — in cli and mcp ## How an ID is spelled — in intercept_id ## Line ranges — in intercept_line_ranges [... per-language ID rules, with examples ...] ## What a read returns [... the two sections, with a worked output example ...] ## Writing descriptions — mcp only ## How the graph models this codebase [... per-language graph semantics ...] ## Guidelines 1. **Reach for a declaration before a file, and a read before a search.** [...] 2. **Ask for everything you need at once.** [...] 3. **Descriptions are usually sufficient.** [...] 4. **Do not re-read.** If it is already in your context, use it. [... four more ...]
mcp mode's high contract additionally carries a Writing
descriptions section: how to work node_list_no_description in batches
through the executor sub-agents.
Slash commands
Names are identical in Claude Code and OpenCode; OpenCode variants additionally declare which agent runs them, since a subtask there cannot spawn further subtasks.
/descriptions-generate
Generate descriptions for all targeted undocumented resources, orchestrated from the main session. The command instructs the main agent to:
- List targeted undocumented resources — prefer
node_list_no_description, elsearac resource list --no-description. - Split them into deterministic, non-overlapping batches of at most the configured batch size, every ID in exactly one batch.
- Launch one
descriptions-generation-executorsub-agent per batch, all in a single concurrent wave, giving each only its assigned IDs, names and kinds. - Re-list undocumented resources when the wave finishes — the database is the source of truth, not the executors' self-reports.
- Re-batch and re-launch anything still undocumented, repeating until none remain, then report the IDs that kept failing.
Batch size comes from
llm.<harness>.agents.descriptions-generation-executor.params.max-batch-size
and is baked into the command text at init time. Default 5.
/descriptions_clear [args]
Runs arac descriptions clear $ARGUMENTS and reports how many descriptions
were cleared. Arguments are passed through exactly — e.g.
--target function,struct.
Hooks & plugins
The guard and the pre-tool scan are installed unconditionally, independent
of the plugins config. The edit-sync hook is installed only when the main agent
lists the edit-update-db-plugin plugin.
| Hook / plugin | Harness | Event | Runs | Installed |
|---|---|---|---|---|
arac-guard.sh | Claude Code | PreToolUse + PostToolUse, matcher Read|Grep|Edit|Write|Bash |
arac guard --claude-hook | always |
arac-pre-tool-scan.js | OpenCode | tool.execute.before |
arac guard --pre-scan | always |
arac-update-file.sh | Claude Code | PostToolUse, matcher Edit|Write|MultiEdit |
arac update-file --claude-hook | edit-update-db-plugin |
arac-native-edit-sync.js | OpenCode | native edit | topology sync | edit-update-db-plugin |
What the guard hook does
- Pre-call, every mode: runs the
scan.pre_toolscan. - Rewrites a search, every mode. A single, unpiped, unredirected Bash
grepthat the parser models comes back asarac cmd -- <the original text>, verbatim. - Rewrites a read in
intercept_idandintercept_line_ranges, when the topology knows the target. - Denies per
blocked_tools, inmcpandclionly. A denied read is answered with its content where it can be. Blockinggrepalso blocksrg/Select-Stringrun via Bash; blockingbashblocks the Bash tool entirely; piped reads (cmd | grep) are exempt unlessread.pipe_passthrough: false. - Post-call nudge on a native
Read/Grep/Edit/Write, which interception never sees. A shell read gets none.
Guard rails on the rewrite: never a second time, never across a pipe, a
redirect, a heredoc, an &&, an env prefix or a wrapper; never a mutation;
never a file with no topology nodes. See
what passes through.
The guard fails open. Any read or parse error emits nothing, so the tool call proceeds and the session is never broken. The pre-scan plugin always exits 0.
Generated sub-agents
descriptions-generation-executor
Generates descriptions for one assigned batch of undocumented topology resources. Reads
each resource, writes a concise description through update_description, and
nothing else. Declares its own MCP server inline
(arac serve --tool-profile descriptions-generation-executor), so it works on
every surface including the terminal ones.
Model defaults to haiku under claude_code. Other harnesses
inherit the main agent's model.
14The Viz menu Full build
arac viz serve # → http://127.0.0.1:7331
arac viz serve --addr 0.0.0.0:8080 --db ./other/topology.dbA local HTTP server serving a single-page app embedded in the binary. It makes no network requests, and binds to localhost by default.
The Visualization screen
Visualization mode
| Mode | What it renders |
|---|---|
| Packages & Modules | Import relationships: Go packages (package→package), and Python/JS/TS modules as files (file→file). The orientation view — the shape of the project in one screen. |
| Data Flow | How values move: the call and use edges between declarations, rather than the containment structure. |
| Custom | Exposes every filter for hand-built graph slices — see below. |
Left panel controls
- Language
- Filter to one language, or
All. Useful in a polyglot repo where the merged graph is too dense to read. - Search
- Matches function names, paths and descriptions — the same three-way search the CLI grep does. Selecting a hit centres the graph on it.
- Neighborhood depth
1–4. With a node selected, renders only what is reachable within that many hops. This is the control that makes a 5,000-node graph legible.- Graph optimization
- Opens the optimization-rule editor: add, toggle and favourite rules that collapse or hide noise.
- Zoom / Fit
+,−andFit, above the canvas, alongside a status line with the node and edge counts.
Custom mode filters
- Kind
- Multi-select over
package,file,function,method,struct,named_type,interface,variable,dependency. - Edge kind
- Multi-select over the edge types present in this topology.
- Strict edges
- Keep only edges whose both endpoints survived the node filters, instead of drawing a dangling edge to a filtered-out node.
- Path
- Restrict to a subtree, e.g.
internal/topology. - Limit
- Max nodes to render, 1–3000. Default 300.
Inspector & Code tabs
Selecting a node fills the right-hand panel. Inspector shows the ID, kind, language, path and line span, the description, the typed properties, in/out degree and warning count, and — for a collapsed node — what it includes. Code shows the node's source. Graph nodes are keyboard-reachable: tab to them, enter or space to select.
Optimization rules
Rules live in the JSON file named by
viz.graph.optimization_rules (default
.aracne/optimization_rules.json) and are editable from the UI. A rule is a list
of operations ANDed together; a node matching all of them is collapsed out of
the rendered graph. Two default rules ship, both aimed at unexported leaf noise.
Operation kind | Takes value | Matches when |
|---|---|---|
non_exported | — | The identifier is unexported / private |
resource_kind | a kind name | The node is of that kind |
less_equal_incoming | integer | In-degree ≤ value |
less_equal_outgoing | integer | Out-degree ≤ value |
less_equal_connections | integer | In-degree + out-degree ≤ value |
less_equal_lines | integer | The declaration spans ≤ value lines |
[
{
"id": "default-non-exported-leaf",
"active": true,
"favorite": true,
"operations": [
{ "kind": "non_exported" },
{ "kind": "less_equal_incoming", "value": "1" },
{ "kind": "less_equal_outgoing", "value": "0" }
]
}
]The Settings page
Reachable from the gear in the top-right. It exposes two things that are genuinely easier in a UI than in JSON:
- Need Description — which resource kinds the no-description tools
and description workflows target. Writes
descriptions.kinds. - Provider Settings — add OpenAI, Anthropic, DeepSeek or a
Custom provider. A custom provider takes a base URL and a
chat contract (which of the three wire formats it speaks) plus a list of possible
models, so a gateway, a proxy or a self-hosted model works. Credentials are stored either
as a literal key or as the name of an environment variable to read, in
.aracne/providers.json.
HTTP API
The SPA is a client of a plain JSON API, which is equally usable from
curl or your own tooling.
| Route | Query | Returns |
|---|---|---|
GET /api/summary | — | Root, language(s), node/edge/warning counts, per-kind and per-edge-type histograms, generation time |
GET /api/graph | mode, q, path, language, kind, edge_kind, strict_edges, limit, optimization_rules | A graph slice: nodes with kind, path, line span, description, degrees, and edges |
GET /api/neighborhood | id, depth (0–4), direction, mode, kind, edge_kind, limit (≤3000), optimization_rules | The subgraph around one node |
GET /api/search | q, path, language, optimization_rules | Ranked node matches over names, paths and descriptions |
GET /api/node/<id> | — | One node in full, with its source |
GET /api/warnings | — | Outstanding topology warnings |
GET /api/config | — | The effective config the UI edits |
GET|PUT /api/optimization-rules | — | Read and write the rule file |
GET /api/ws | — | WebSocket for live updates and streaming |
15Configuration — .aracne/config.json
Written by arac init, rendered into integration files by
arac setup, and read by everything. An invalid file is overwritten with
defaults. Every tool name in it is validated against the
tool catalog at load time, so a typo fails fast instead of silently disabling a tool.
arac setup exits non-zero and names the offending key on an invalid config.
mode
modestringdefault "cli"The one dial. One of cli, intercept_id,
intercept_line_ranges, mcp. Question two of
arac init. An unknown value is reported by validation and
falls back to the default. See The four modes.
contract_verbosity
contract_verbositystringdefault "low"How much the generated contract says: low (the default) or
high. Question six of arac init. An unknown
value is rejected by validation.
CLAUDE.md, AGENTS.md and Aracne's own agent harness all get the
same document. See The injected contract for what each verbosity
contains.
preload_mcp_tools
preload_mcp_toolsboolean or absentdefault absenttrue, false, or absent. Claude Code only, and
mode: "mcp" only.
Setting this to true makes arac setup write
{ "env": { "ENABLE_TOOL_SEARCH": "false" } } into
.claude/settings.json, so every tool arrives with its schema.
Absent means Aracne does not manage the variable, so a project that
predates this key — or one whose operator set ENABLE_TOOL_SEARCH
themselves — is left exactly as it is.
The switch belongs to Claude Code and is not per-server: it loads every tool in the session up front, including any from other MCP servers.
scan & scanner
scan.pre_toolstringdefault "default"The scan the guard runs before every tool call it sees, on both
harnesses. default is an incremental scan, so the usual case (nothing changed
since the last call) is a no-op. none switches the freshness guarantee off, for
projects keeping the topology current another way — e.g. arac scanner run.
full exists for the same reason arac scan has it, not because a hook
should normally use it. "hard" scan mode is rejected here.
An unrecognized value resolves to default.
scan.ignorestring[]default [].gitignore-style globs. Any matching path is skipped by the scanner on every
front — file discovery, manifest and parsing in every mode — so ignored files
never enter the topology.
Supports *, **, ?, and a trailing /
for directory-only. A pattern with no / floats at any depth. There is no
! negation — use paths
when you need to re-include a subtree.
"ignore": ["dist/", "*.generated.go", "**/fixtures/**"]
An ignored tree is invisible to every walk, not just the parsing one — language detection prunes it too.
scan.workersintegerdefault 0 (auto = NumCPU)Max files parsed concurrently during a full scan. Lower it to cap peak RAM on large
projects. --workers overrides per run.
scan.progressstringdefault "auto"auto (shown on a terminal when the project has more than 15 files),
always, or never. --progress overrides per run.
scanner.update_frequencyinteger (ms)default 200How often arac scanner run polls for changes.
read
read.max_file_sizeinteger (bytes)default 524288 (512 KB)Files above this are not read or indexed.
read.kindsstring[]default ["file","function","struct","interface"]Allow-list of resource kinds that may be read. Also accepts named_type,
package, dependency, variable. There is no
method — methods resolve as functions.
It gates every read entrance, in every mode — the MCP
read tool, arac read, an intercepted shell read (cat,
head, sed -n, by path or by resource ID) and the denial
proxy.
An ID resolving to a kind outside the list is refused, naming the kind
and the allowed set, and the refusal exits non-zero rather than falling back
to the plain command. An operand that resolves to nothing is a different answer and
still passes through. --kind narrows which
resource an ID resolves to; it does not grant a kind the list excludes.
Absent means the default set. An explicit [] would make read
useless and is rejected by validation.
read.file_modestringdefault "skeleton", except "full" in mcp modeWhat a whole-file read returns. full is the file verbatim;
skeleton is each top-level declaration's signature with large bodies replaced by
an elision marker naming the ID to read.
read.skeleton_thresholdinteger (lines)default 12How many lines a declaration may span before skeleton elides its body. The
default keeps small helpers, constructors and one-line accessors intact. Non-positive uses the
default.
read.max_symbol_linesinteger (lines)default 160; 0 disablesCaps a symbol body the same way. Past it the read returns the signature plus an
elision marker naming the ID, and the model can ask again with full.
0 means no cap.
read.pipe_passthroughbooleandefault trueExempt read/grep shell commands that consume piped stdin (cmd | tail) from
the guard: such commands operate on command output, which Aracne cannot serve. Direct file
reads are still gated. Set false to gate piped reads alongside them.
read.context_filter
read.context_filterstringdefault "normal"How verbosely the # CONTEXT: block renders a read's neighbours. One of:
off— the code asked for, and nothing around it.normal— each neighbour asid: description. Neighbours with no description are omitted: the block earns its tokens by letting the model skip a read, and a description is what says whether the neighbour matters, so an entry without one costs a line and answers nothing. Constants are exempt — a value shown is a description.full— neighbours as fenced source cuts, undescribed ones kept, plus a# USED BY:section listing the resources that reference this one.
This was six independent sub-keys — include_incoming,
external_vars_visibility, small_functions_visibility,
small_function_threshold, hide_no_description and
max_inline_parent_lines. They were never independent in practice: a project
wants a terser or a fuller context block, not one neighbour kind elevated while another is
suppressed. An unrecognized value is rejected by validation and resolves to
normal.
grep
grep.description_kindsstring[]default ["function","method","struct","interface"]Which kinds may match the pattern on their stored description. A node found this way is returned even when its source contains no matching line — which is the whole point, since descriptions live only in the topology database.
The same set Aracne writes descriptions for. file,
package and variable are excluded by default: their descriptions are
thin and flood results. An explicit [] disables description matching entirely.
terminal
terminal.max_overserveintegerdefault 4; 0 or negative disablesAn answer must stay within this multiple of the bytes the plain answer it replaces would
have cost. Every surface that answers in something else’s place measures itself here:
intercepted commands, the guard’s proxied read, and the read and
grep tools.
Over the ceiling a shell command runs for real, and a tool serves the plain answer instead — source without its context block, matches without their node rows. Small answers always pass (12KB for a read, 2KB for a search) and nothing exceeds 32KB; a search that matched only names and descriptions is exempt, having no plain answer to be measured against.
descriptions
descriptions.kindsstring[]default ["function","method","struct","interface"]Which kinds the description workflows target and the no-description tools list. Editable from the Viz Settings page.
descriptions.style_exemplarsintegerdefault 1How many already-written neighbour descriptions to feed the executor as house-style
anchors. 0 disables, saving tokens.
descriptions.include_not_visiblebooleandefault falseWhen false, skip undocumented targets that
read.context_filter would not render anyway.
Who writes the descriptions
One provider block covers both the sweep and the lazy fill. See Who writes them for the CLI setup.
descriptions.providerstringdefault unansweredanthropic, openai, deepseek, or cli to
run cli_provider_command. Absent means nobody has been asked yet, which is what
arac init asks and what the sweep refuses on. A typo, or
cli with no command, is rejected by validation.
The model is not set here. It is
llm.<any>.agents.descriptions-generation-executor.model, which
arac init asks for.
descriptions.api_key_envstringdefault the provider's own nameThe environment variable an API key is read from — useful when a gateway speaks one
provider's wire format but bills its own credential. Absent falls back to
ANTHROPIC_API_KEY,
OPENAI_API_KEY or DEEPSEEK_API_KEY. Ignored by cli,
which has no key.
descriptions.base_urlstringdefault the provider's ownPoint an API provider at a gateway, a proxy, or a self-hosted model speaking one of the three wire formats. Ignored by the CLI transports, which have no endpoint.
descriptions.cli_provider_commandstringrequired by provider: "cli"The command to run, e.g. "claude -p". It is argv, not a shell
line: words split on whitespace, quotes group, backslash escapes, and there are no
pipes, no redirection and no $VAR. The prompt arrives on the command's stdin;
the descriptions are read from its stdout.
descriptions.lazy
Accepts both JSON shapes: "lazy": true as a plain switch, or
"lazy": {"enabled": true, "max_nodes": 8} with the tuning exposed. It is written
back in whichever shape you used.
| Field | Type | Default | What it does |
|---|---|---|---|
enabled | boolean | true | The switch. |
max_nodes | integer | 40 | Nodes one fill may describe. ≤ 0 means no cap. |
timeout_seconds | integer | 45 | Bounds the whole fill, not one batch. ≤ 0 disables the deadline. |
batch_size | integer | 5 | Resources per completion. Non-positive keeps the default. |
parallel | integer | 4 | Batches in flight at once. Non-positive keeps the default. |
// the common case "descriptions": { "lazy": true } // no API key: both the sweep and the lazy fill go through the CLI "descriptions": { "provider": "cli", "cli_provider_command": "claude -p --model haiku --max-turns 1", "lazy": { "enabled": true, "max_nodes": 12, "timeout_seconds": 45 } }
llm
Per-harness agent configuration. Three blocks: <any>,
opencode, claude_code — each with a
main_agent and a map of named agents. This is what
arac setup and arac serve --tool-profile consult to decide what each
agent can do.
Resolution order
- The per-harness block (
claude_codeoropencode) wins over<any>. - An absent field, or the sentinel
"<inherits>"as a model, falls back to the main agent. <any>applies to both harnesses.
Agent fields
| Field | Type | What it does |
|---|---|---|
model | string | Model for this agent. "<inherits>" copies the main agent's. |
mcp_tools | string[] | Which MCP tools this agent gets. Validated against the catalog. Filtered through ServableMCPTools, which drops the shell-served ones and returns nothing outside mcp mode. |
blocked_tools | string[] | Native tools to deny: read, grep, edit, write, bash. Empty by default. Only bites in mcp and cli; grep is additionally dropped outside mcp. Blocking read also renames the Aracne read tool from read_resource to read. |
plugins | string[] | Currently one: edit-update-db-plugin, which installs the native-edit topology-sync hook. An unknown name is skipped with a warning. |
params | map<string,int> | Integer knobs. max-batch-size for the description executor; thinking (a reasoning-token budget) for chat sub-agents. A non-positive value falls back to the default. |
"llm": { "<any>": { "main_agent": { "mcp_tools": ["read", "warnings_list"], "blocked_tools": [], "plugins": ["edit-update-db-plugin"] }, "agents": { "descriptions-generation-executor": { "model": "<inherits>", "mcp_tools": ["read", "update_description"], "params": { "max-batch-size": 5 } } } }, "claude_code": { "main_agent": {}, "agents": { "descriptions-generation-executor": { "model": "haiku" } } }, "opencode": { "main_agent": {} } }
viz
viz.graph.optimization_rulesstring (path)default ".aracne/optimization_rules.json"Where the graph optimization rules live.
paths
pathsarray of {path, hidden}default []Rules marking directories or files (relative to the topology root) hidden or visible. Hidden paths are skipped by the indexing stage (file discovery, manifest) and the scan stage in every mode.
More specific rules win, so a parent can be hidden while a nested child
stays visible — which is the thing scan.ignore cannot express, since it
has no negation.
"paths": [ { "path": "my_example", "hidden": true }, { "path": "my_example/another_layer", "hidden": false } ]
Full example
What a project looks like after arac init, annotated. The
seven answers are mode, contract_verbosity, the
descriptions provider block, the executor's model and
preload_mcp_tools; everything else is a default.
{
"mode": "cli",
"contract_verbosity": "low",
"scan": {
"ignore": [],
"workers": 0,
"progress": "auto",
"pre_tool": "default"
},
"scanner": { "update_frequency": 200 },
"terminal": { "max_overserve": 4 },
"read": {
"max_file_size": 524288,
"kinds": ["file", "function", "struct", "interface"],
"pipe_passthrough": true,
"context_filter": "normal"
},
"grep": {
"description_kinds": ["function", "method", "struct", "interface"]
},
"descriptions": {
"kinds": ["function", "method", "struct", "interface"],
"style_exemplars": 1,
"lazy": true,
"provider": "anthropic",
"api_key_env": "ANTHROPIC_API_KEY"
},
"llm": {
"<any>": {
"main_agent": {
"mcp_tools": ["read", "warnings_list"],
"blocked_tools": []
},
"agents": {
"descriptions-generation-executor": {
"model": "claude-haiku-4-5",
"mcp_tools": ["read", "update_description"],
"blocked_tools": [],
"params": { "max-batch-size": 5 }
}
}
},
"opencode": { "main_agent": {}, "agents": {} },
"claude_code": { "main_agent": {}, "agents": {} }
},
"viz": {
"graph": { "optimization_rules": ".aracne/optimization_rules.json" }
},
"paths": []
}16Environment variables
| Variable | Read by | What it does |
|---|---|---|
ANTHROPIC_API_KEY | description generation | Anthropic provider credential, unless descriptions.api_key_env names another variable. |
OPENAI_API_KEY | description generation | OpenAI provider credential, unless descriptions.api_key_env names another variable. |
DEEPSEEK_API_KEY | description generation | DeepSeek provider credential, unless descriptions.api_key_env names another variable. |
ARAC_NO_PARTIAL | the scanner | Set to any value to disable the partial-incremental fast path and force every incremental scan down the full two-phase route. A debugging escape hatch, not a tuning knob. |
CLAUDE_PROJECT_DIR | the hooks | Set by Claude Code. The generated hook commands are written as ${CLAUDE_PROJECT_DIR}/.claude/hooks/arac-guard.sh so they resolve regardless of the agent's working directory. |
CGO_ENABLED | the build | Must be 1. The Makefile exports it. |
No key is needed for anything but descriptions. Scanning, reading, grepping, the guard, the visualizer and the MCP server all work with an empty environment — the MCP host brings its own model. And descriptions themselves need no key either if you point them at a CLI you are already logged into. Lazy description generation with nothing configured to write with is a silent no-op, not an error.
Credentials configured through the Viz Settings page are stored in
.aracne/providers.json and can be recorded as the name of an
environment variable rather than a literal key.
17Language support
| Language | Parser | CGO | Model |
|---|---|---|---|
| Go | stdlib go/ast | no | Package-first. The richest support: cross-package return-type inference, interface↔struct matching, generics, embedding. |
| Python | custom, no external grammar | no | Modules-first (file IDs, imports_module). ABC/Protocol/dataclass, inheritance, decorators, cross-module resolution. |
| JavaScript | tree-sitter | yes | Modules-first. Classes, extends, getters/setters, static, default and named exports, CommonJS require, JSX. |
| TypeScript | tree-sitter (shared with JS) | yes | Modules-first. Adds interfaces, type aliases, enums, and annotation-driven method resolution. |
| Rust | tree-sitter | yes | Module-path IDs, plus a cross-file impl mechanism so methods defined away from their type still attach to it. |
| Java | tree-sitter | yes | FQN + signature IDs, struct↔struct inherits, and the awkward declaration forms: enum constants with bodies, records, anonymous and local classes, initializer blocks. |
Known quirks worth knowing
- JS and TS are independent topologies. There is no cross-language import
resolution between them — a
.tsfile importing a.jsone does not produce an edge. - Python classes are stored with kind
struct, and an ABC subclass yieldsinherits/inherited_by, notimplements. - Python
uses_dependencyis signature-scoped. The scanner records the edge only for imports used in a signature, annotation or decorator — never for ones used only in a function body.json.dumps(...)inside a body produces no edge and noimportline in a read. - Go resource IDs are module-path-prefixed. They begin with whatever the
moduleline ingo.modsays. - Interface conflicts are only detected where declaring one is a claim.
Go and Python
Protocolare structural — a broken type simply has noimplementsedge.
Files outside the six languages
They are not indexed, so a read or grep on them passes through to the real command unchanged.
18Troubleshooting
The build fails to link the scanners
CGO is off or there is no C compiler. Install gcc and make sure
CGO_ENABLED=1. make build sets it for you.
arac viz serve says "this is the Basic build"
You have the Basic binary. Install the Full build:
go install github.com/Rhuan-Marques/aracne/cmd/arac@latest, or grab the release
asset without the -basic suffix.
Reads and greps are not being answered from the topology
Check, in order:
arac check-updates— is the index stale, or was the project never scanned?- The mode. Shell reads are only intercepted in
intercept_idandintercept_line_ranges. Search is intercepted in every mode. - Whether the command has a flag Aracne does not model, or is piped, redirected, or part of a compound command — those pass through by design.
- Whether the target file has any topology nodes at all. An unindexed language, an ignored path, or a hidden path all mean passthrough.
- Whether the answer would exceed
terminal.max_overserve. - That the hook is installed — re-run
arac setupand restart the harness. Both harnesses need a restart to pick up new hooks and MCP config.
The agent's reads are missing descriptions
read.context_filter is "normal", so undescribed neighbours are omitted
rather than shown blank. Either warm the repo (arac descriptions generate), or
let lazy descriptions do it as you work — which needs a provider key
in the environment. Check arac node count --no-description to see how much is
left.
Descriptions disappeared after a re-scan
--hard clears them. Use --all instead, which preserves them, and
keep a sidecar: arac descriptions export before, import after.
--min-rate on the import will tell you if the sidecar no longer fits the graph.
A warning will not go away
signature_changed and interface_conflict are derived from current
state, so they retire themselves once the code actually fits — but they need a scan to
notice. Run arac scan. If it persists, arac warnings list --kind
signature_changed names the caller to verify in its TargetID.
The topology has duplicate ../<dir>/… nodes
The database records an absolute root. A copied .aracne/
resolves edited paths against the old directory and silently creates a parallel set of nodes
instead of updating the real ones — which produces no warnings at all and looks like a
bug. Delete .aracne/ and re-scan after moving or copying a project.
An edit produced no topology update
Native edits are synced by the edit-update-db-plugin hook, which is only
installed when the main agent lists that plugin. Either add it to
llm.<harness>.main_agent.plugins and re-run arac setup, or rely
on the pre-tool-call scan (scan.pre_tool), which catches the drift before the
next call reads it.
The scan uses too much memory
Lower scan.workers (or pass --workers 2). A full scan parses files
concurrently, one per CPU by default. Adding build output to scan.ignore usually
helps more.
arac setup exits with "Invalid .aracne/config.json"
The message names the offending key and, for a tool name, the agent it was found under.
Common causes: a tool name that is not in the catalog, an unknown mode, an empty
read.kinds, or an unknown descriptions.provider.
Further reading
| In the repository | What it is |
|---|---|
README.md | The short version of this page. |
docs/modes.md | The four modes in more detail. |
docs/configuration.md | Every key in .aracne/config.json. |