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.

Go 1.25 Apache-2.0 6 languages Binary: arac

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

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.

BuildContainsBuild commandFront-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).

shell
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

shell
go install github.com/Rhuan-Marques/aracne/cmd/arac@latest

go install produces the Full build.

Build from source

shell
git clone https://github.com/Rhuan-Marques/aracne && cd aracne
make build         # Full  → bin/arac
make build-basic   # Basic → bin/arac-basic

03Quickstart

shell
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

KindWhat it isNotes
packageA Go packagePython/JS/TS/Rust are modules-first and drop this node in favour of file-first IDs
fileOne source fileCarries the file's top-level declarations as containment edges
functionA free function
methodA function bound to a typeResolves as a function for read purposes
structA struct, class or recordPython and JS/TS classes are stored as struct
interfaceAn interface, trait or Protocol/ABC
named_typeA defined type, type alias or enum
variableA package/module-level variable or constant
dependencyAn external module the project importsWhat 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".

GroupEdges
References (outgoing)calls, uses_struct, uses_class, uses_interface, uses_named_type, uses_extvar, uses_package, uses_dependency
Containmenthas_file, has_function, has_struct, has_class, has_interface, has_named_type, has_extvar, has_abstract_methods, methods, constructor
Type relationshipsimplements / implemented_by, inherits / inherited_by
Importsimports, imports_package, imports_module, imports_dependency

Scan modes

ModeFlagWhat it doesDescriptions
incremental default--defaultDiffs the tree against file_manifest.json and re-parses only what changedpreserved
full--allRe-parses every filepreserved
hard--hardRebuilds the database from scratchcleared, along with bugs

What the scanners skip

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.

LanguageType / functionMethod
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.

Lazy Descriptions might let your tasks slower at first, since it's running description generation in real time. As descriptions build up, this effect will diminish organically, but you can turn off descriptions.lazy in the config if needed.

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.

KeyWhat it does
provideranthropic, openai, deepseek, or cli to run a command. Absent means unanswered, and arac init asks.
base_urlA gateway or proxy for the API providers. Ignored by the CLI ones.
api_key_envThe environment variable the API key is read from. Absent falls back to the provider's own name. Ignored by cli.
cli_provider_commandThe 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.

.aracne/config.json
"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:

shell
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.

shell
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

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.

KindMeans
use_missing_nodeA resource references something that is not in the graph.
node_removedA node that others referenced is gone.
signature_changedA 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_conflictA 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.

modeMCP toolsshell readsshell grepblocked_toolsaddressing
cli default nonerun as themselvesinterceptedactive resource IDs, via arac read
intercept_id noneinterceptedinterceptedinert resource IDs, as command operands
intercept_line_ranges noneinterceptedinterceptedinert path:start-end
mcp one read (+ warnings_list)run as themselvesinterceptedactive 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

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

CommandWindow
cat, less, morewhole file
batwhole file, or --line-range
head -Nfirst N lines
tail -N, tail -n +Nlast N lines / from N
sed -n 'A,Bp'an explicit range
awk 'NR>=A && NR<=B'an explicit range
git show, git cat-filewhole file at a revision
Get-Content (PowerShell)whole file, -TotalCount, -Tail
grep, egrep, fgrep, rg, ack, ag, ugsearch — 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.

A search that found nothing exits 1, the way grep and rg do.

The guard, mode by mode

Behaviourcliintercept_idintercept_line_rangesmcp
Rewrites shell searchyesyesyesyes
Rewrites shell readsnoyesyesno
blocked_tools can denyyesnonoyes
Post-call nudge on a shell readyesnonono
Pre-call freshness scanyesyesyesyes

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.

.mcp.json — written in mcp mode
{
  "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_code overrides.
--harness opencode
Applies the opencode overrides.

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 scan already 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, or never. Default auto
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, or count.
--head-limit <n>
Max matching lines. Default 200; -1 for 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. Default function,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, or never. Default auto
--include-not-visible
Include resources read.context_filter would not render as a normal line — small functions and external variables under the full or hidden settings. Mirrors descriptions.include_not_visible for 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.provider for this run. Quote a command with flags of its own: --cli "codex exec". Bare --cli runs claude -p and asks first.
-y
Auto-confirm the bare --cli prompt.

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 2 when 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 edit

Batch. 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 edit

arac 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 write

arac 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 as descriptions-generation-executor.
--harness <name>
claude_code (default) or opencode — 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_tools denial where one applies, and the post-call nudge. Any read or parse error fails open.
--pre-scan
Runs the configured scan.pre_tool scan against the project. What OpenCode's arac-pre-tool-scan.js plugin 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.

ToolMCPWhat it does
readyesRead any resources by ID — one call takes several, and returns their source plus connected context
warnings_listyesList topology warnings
update_descriptionyesSet a resource's description
node_list_no_descriptionyesList undocumented resources needing descriptions
grepshellSearch node names, descriptions and contents. Not registered as an MCP tool in any mode — the shell form is intercepted everywhere
editshellExact string replacements, batchable, topology synced inline. Shell-served
writeshellCreate or overwrite a file, topology synced inline. Shell-served

Default tool sets

AgentDefault mcp_tools
mainread, warnings_list
descriptions-generation-executorread, 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.

ParameterTypeRequiredDescription
idsarray of stringyesResource 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.
fullbooleannoReturn 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.

ParameterTypeRequiredDescription
patternstringyesRegex to match
pathstringnoFile or directory to search. Default .
globstringnoFilename glob, e.g. *.go, **/*_test.ts
typestringnoLanguage filter: go, py, js, ts, rust, java, c, cpp, md, json, yaml, toml, sh
case_insensitivebooleannoMatch case-insensitively
output_modestringnocontent (default), files_with_matches, count
head_limitintegernoMax matching lines. Default 200; -1 unlimited
before / afterintegernoContext 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.

ParameterTypeDescription
editsarray of objectA 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_pathstringSingle-edit form
old_stringstringExact text to find
new_stringstringReplacement. Empty string deletes the matched text
replace_allbooleanReplace 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.

ParameterTypeRequired
file_pathstringyes
contentstringyes

warnings_list

List outstanding topology warnings: missing references, removed resources, changed signatures, interface conflicts. Grouped by kind, with a summary and details.

ParameterTypeDescription
source_idstringFilter by source resource ID
target_idstringFilter by target resource ID
kindstringuse_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.

arac init
   ▄▀█ █▀█ ▄▀█ █▀▀ █▄░█ █▀▀
   █▀█ █▀▄ █▀█ █▄▄ █░▀█ ██▄

  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
#QuestionWrites
1Which harness — Claude Code, OpenCode, or bothnothing; it picks which tree below gets written
2How the model reaches your codemode
3Who writes the descriptions — an API key (then the wire format and the key's variable) or a CLI commanddescriptions.provider + api_key_env or cli_provider_command
4Which model writes themllm.<any>.agents.descriptions-generation-executor.model
5Describe the repository now, or lazily as you readnothing — it decides whether this run sweeps
6How much the contract sayscontract_verbosity
7Keep Aracne's tools loaded in context — asked only on Claude Code with mode: "mcp", the one combination it means anything onpreload_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

FileContentsWhen
CLAUDE.mdThe # Aracne contract block for the project's mode and verbosity — the same document AGENTS.md and Aracne's own agent harness getalways
.mcp.jsonmcpServers.aracnearac serve --tool-profile main --harness claude_codemcp mode only — and removed when the mode moves away from mcp
.claude/settings.jsonPreToolUse + 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 onalways (merged, preserving your own hooks)
.claude/hooks/arac-guard.sh (.ps1 on Windows)arac guard --claude-hookalways
.claude/hooks/arac-update-file.sharac update-file --claude-hookonly with the edit-update-db-plugin plugin
.claude/commands/*.mdSlash commandsalways (bug ones gated)
.claude/agents/*.mdSub-agents, each with its own inline mcpServers:always (bug ones gated)

OpenCode

FileContentsWhen
AGENTS.mdThe # Aracne contract block, byte-identical to the one written into CLAUDE.mdalways
.opencode/opencode.jsonmcp.aracnearac 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_toolsMCP entry in mcp mode; permissions always
.opencode/plugins/arac-pre-tool-scan.jstool.execute.beforearac guard --pre-scanalways, unconditionally
.opencode/plugins/arac-native-edit-sync.jsTopology sync after a native editonly with the edit-update-db-plugin plugin
.opencode/commands/*.mdSlash commandsalways (bug ones gated)
.opencode/agents/*.mdSub-agentsalways (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.

CLAUDE.md — the cli contract at "low", in full
# 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.

CLAUDE.md — the cli contract at "high", abridged
# 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:

  1. List targeted undocumented resources — prefer node_list_no_description, else arac resource list --no-description.
  2. Split them into deterministic, non-overlapping batches of at most the configured batch size, every ID in exactly one batch.
  3. Launch one descriptions-generation-executor sub-agent per batch, all in a single concurrent wave, giving each only its assigned IDs, names and kinds.
  4. Re-list undocumented resources when the wave finishes — the database is the source of truth, not the executors' self-reports.
  5. 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 / pluginHarnessEventRunsInstalled
arac-guard.shClaude Code PreToolUse + PostToolUse, matcher Read|Grep|Edit|Write|Bash arac guard --claude-hookalways
arac-pre-tool-scan.jsOpenCode tool.execute.before arac guard --pre-scanalways
arac-update-file.shClaude Code PostToolUse, matcher Edit|Write|MultiEdit arac update-file --claude-hookedit-update-db-plugin
arac-native-edit-sync.jsOpenCode native edit topology syncedit-update-db-plugin

What the guard hook does

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

shell
arac viz serve                              # → http://127.0.0.1:7331
arac viz serve --addr 0.0.0.0:8080 --db ./other/topology.db

A 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

ModeWhat it renders
Packages & ModulesImport 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 FlowHow values move: the call and use edges between declarations, rather than the containment structure.
CustomExposes 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
14. 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
+, and Fit, 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 kindTakes valueMatches when
non_exportedThe identifier is unexported / private
resource_kinda kind nameThe node is of that kind
less_equal_incomingintegerIn-degree ≤ value
less_equal_outgoingintegerOut-degree ≤ value
less_equal_connectionsintegerIn-degree + out-degree ≤ value
less_equal_linesintegerThe declaration spans ≤ value lines
.aracne/optimization_rules.json
[
  {
    "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:

HTTP API

The SPA is a client of a plain JSON API, which is equally usable from curl or your own tooling.

RouteQueryReturns
GET /api/summaryRoot, language(s), node/edge/warning counts, per-kind and per-edge-type histograms, generation time
GET /api/graphmode, q, path, language, kind, edge_kind, strict_edges, limit, optimization_rulesA graph slice: nodes with kind, path, line span, description, degrees, and edges
GET /api/neighborhoodid, depth (0–4), direction, mode, kind, edge_kind, limit (≤3000), optimization_rulesThe subgraph around one node
GET /api/searchq, path, language, optimization_rulesRanked node matches over names, paths and descriptions
GET /api/node/<id>One node in full, with its source
GET /api/warningsOutstanding topology warnings
GET /api/configThe effective config the UI edits
GET|PUT /api/optimization-rulesRead and write the rule file
GET /api/wsWebSocket 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 absent

true, 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 200

How 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 mode

What 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 12

How 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 disables

Caps 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 true

Exempt 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 as id: 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 disables

An 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 1

How many already-written neighbour descriptions to feed the executor as house-style anchors. 0 disables, saving tokens.

descriptions.include_not_visiblebooleandefault false

When 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 unanswered

anthropic, 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 name

The 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 own

Point 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.

FieldTypeDefaultWhat it does
enabledbooleantrueThe switch.
max_nodesinteger40Nodes one fill may describe. ≤ 0 means no cap.
timeout_secondsinteger45Bounds the whole fill, not one batch. ≤ 0 disables the deadline.
batch_sizeinteger5Resources per completion. Non-positive keeps the default.
parallelinteger4Batches 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

  1. The per-harness block (claude_code or opencode) wins over <any>.
  2. An absent field, or the sentinel "<inherits>" as a model, falls back to the main agent.
  3. <any> applies to both harnesses.

Agent fields

FieldTypeWhat it does
modelstringModel for this agent. "<inherits>" copies the main agent's.
mcp_toolsstring[]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_toolsstring[]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.
pluginsstring[]Currently one: edit-update-db-plugin, which installs the native-edit topology-sync hook. An unknown name is skipped with a warning.
paramsmap<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.

.aracne/config.json
{
  "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

VariableRead byWhat it does
ANTHROPIC_API_KEYdescription generationAnthropic provider credential, unless descriptions.api_key_env names another variable.
OPENAI_API_KEYdescription generationOpenAI provider credential, unless descriptions.api_key_env names another variable.
DEEPSEEK_API_KEYdescription generationDeepSeek provider credential, unless descriptions.api_key_env names another variable.
ARAC_NO_PARTIALthe scannerSet 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_DIRthe hooksSet 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_ENABLEDthe buildMust 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

LanguageParserCGOModel
Gostdlib go/astnoPackage-first. The richest support: cross-package return-type inference, interface↔struct matching, generics, embedding.
Pythoncustom, no external grammarnoModules-first (file IDs, imports_module). ABC/Protocol/dataclass, inheritance, decorators, cross-module resolution.
JavaScripttree-sitteryesModules-first. Classes, extends, getters/setters, static, default and named exports, CommonJS require, JSX.
TypeScripttree-sitter (shared with JS)yesModules-first. Adds interfaces, type aliases, enums, and annotation-driven method resolution.
Rusttree-sitteryesModule-path IDs, plus a cross-file impl mechanism so methods defined away from their type still attach to it.
Javatree-sitteryesFQN + 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

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:

  1. arac check-updates — is the index stale, or was the project never scanned?
  2. The mode. Shell reads are only intercepted in intercept_id and intercept_line_ranges. Search is intercepted in every mode.
  3. 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.
  4. Whether the target file has any topology nodes at all. An unindexed language, an ignored path, or a hidden path all mean passthrough.
  5. Whether the answer would exceed terminal.max_overserve.
  6. That the hook is installed — re-run arac setup and 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 repositoryWhat it is
README.mdThe short version of this page.
docs/modes.mdThe four modes in more detail.
docs/configuration.mdEvery key in .aracne/config.json.