Skip to content

Configuration ​

n-dx uses a layered configuration system across its packages. The ndx config command provides unified access.

LLM Setup During Init ​

ndx init includes a guided LLM configuration flow. In an interactive terminal it presents keyboard-driven selectors (arrow keys to navigate, Enter to confirm) for both provider and model. The flow follows a strict precedence order:

  1. Explicit CLI flags — skip all prompts
  2. Existing project config (.n-dx.json) — reuse previous selections
  3. Interactive prompt (TTY only) — ask the user
  4. Runtime default fallback — handled by downstream packages

On re-init, if the vendor and model are already configured the LLM prompts are skipped entirely.

Available Models ​

VendorModel IDLabelDefault
claudeclaude-sonnet-5-5Claude Sonnet 5.5yes
claudeclaude-opus-5-5Claude Opus 5.5
claudeclaude-fable-5-1Claude Fable 5.1
claudeclaude-haiku-4-5Claude Haiku 4.5
codexgpt-5.6-terraGPT-5.6 Terrayes
codexgpt-5.6-solGPT-5.6 Sol
codexgpt-5.6-lunaGPT-5.6 Luna
codexgpt-5.5GPT-5.5
googlegemini-2.5-proGemini 2.5 Proyes
googlegemini-3.7-flashGemini 3.7 Flash
googlegemini-3.5-flash-liteGemini 3.5 Flash Lite
local(discovered at runtime)——

For the local vendor there is no static catalog — available models are fetched live from the running LM Studio / Ollama server at /v1/models during ndx init. If the server isn't running, the model prompt is skipped.

Gemini's heavy tier stays on gemini-2.5-pro (the newest stable Pro model) rather than gemini-3.1-pro-preview, because preview model IDs can be renamed or withdrawn. You can still select a preview model explicitly with ndx config llm.google.model gemini-3.1-pro-preview ..

The model catalog is curated and local — init works offline without querying vendor APIs. Unknown model IDs are accepted with a warning, so you can use models not yet in the catalog.

Non-Interactive (Flag-Based) Configuration ​

Three flags control LLM selection without prompts:

FlagDescription
--provider=<claude|codex|google|local>Set the active LLM vendor
--model=<model-id>Set the model for the active vendor
--claude-model=<model-id>Set the Claude model (independent of active vendor)
--codex-model=<model-id>Set the Codex model (independent of active vendor)

Examples:

sh
# Fully non-interactive init (CI / scripting)
ndx init --provider=claude --model=claude-sonnet-5-5 .

# Configure both vendors in a single call
ndx init --provider=claude --claude-model=claude-sonnet-5-5 --codex-model=gpt-5.6-terra .

# A lone vendor-specific flag implies the provider
ndx init --claude-model=claude-opus-5-5 .   # implies --provider=claude

Flag rules:

  • --model cannot be combined with --claude-model or --codex-model (ambiguous target vendor).
  • --claude-model and --codex-model can be used together. When both are present without --provider, the active vendor falls through to existing config or the interactive prompt.
  • A lone --claude-model implies --provider=claude; a lone --codex-model implies --provider=codex.

Config Keys (.n-dx.json) ​

LLM settings are persisted under the llm namespace in .n-dx.json:

KeyTypeDescription
llm.vendor"claude" | "codex" | "google" | "local"Active LLM vendor
llm.claude.modelstringClaude model ID
llm.codex.modelstringCodex model ID

Example .n-dx.json after init:

json
{
  "llm": {
    "vendor": "claude",
    "claude": {
      "model": "claude-sonnet-5-5"
    }
  }
}

Robot Wrangler (dashboard) ​

The dashboard's Robot Wrangler settings page (the cog in the bottom bar) edits the same LLM keys. It reads top to bottom:

  1. Vendor — Claude, Codex, Gemini or Local, with each vendor's readiness.
  2. Connection (Server for Local) — CLI or API per provider; for Local, the server profiles, host and port, and a connection test.
  3. Models — the model for each tier (Primary, Standard, Light).
  4. If a run fails — the failover chain.
  5. Review — Off, Self review or Pair review. Pair review has the other vendor's CLI read the diff in a fresh session and report findings without editing any file.

A run card beside the sections shows what a command will use and whether its credentials check out. The save bar at the bottom lists what will change; Discard reverts it and Save stays disabled until something has. At widths under 768px the vendor cards drop from four columns to two; under 640px tier rows and review cards stack.

.codex/config.toml remains MCP-only

Model configuration lives in .n-dx.json, not in .codex/config.toml. The Codex config file is used exclusively for MCP server definitions (stdio transport). ndx init does not write model or vendor settings to .codex/config.toml.

LLM Vendor ​

sh
ndx config llm.vendor claude .    # or: codex
VendorRex BehaviorHench BehaviorToken Accounting
claudeShared LLM client; API when key configured, CLI fallbackBoth api and cli providersFull support
codexCodex CLI adapter (codex exec)CLI-only (api rejected)Limited (CLI doesn't return usage)

Claude Configuration ​

sh
# API mode (recommended)
ndx config llm.claude.api_key sk-ant-... .
# or via environment variable:
export ANTHROPIC_API_KEY=sk-ant-...

# Pin a model (default: claude-sonnet-5-5)
ndx config llm.claude.model claude-opus-5-5 .

# CLI mode
ndx config llm.claude.cli_path /path/to/claude .

Where keys are stored

*.api_key and *.cli_path values are written to .n-dx.local.json, which ndx init gitignores — never to the shared .n-dx.json. Every reader merges the local file over the shared one, so nothing else changes. If a project configured before this routing still has a key in .n-dx.json, ndx config warns on every run and ndx ci fails when that file is git-tracked; re-run ndx config <key> <value> to move it (and rotate the key if it was ever committed).

A *.cli_path or *.api_endpoint in the shared .n-dx.json (and llm.google.apiKeyEnv) applies only once you trust the repository (ndx trust accept .): it picks the binary n-dx runs with your environment, or where your key is sent. Until then n-dx ignores it with a warning naming the field. The same keys in .n-dx.local.json always apply. graview.bin, the binary ndx graview runs, and graview.app, the product face ndx graview serve runs, follow the same rule; NDX_GRAVIEW_BIN always applies.

Hench's claudePath, the fallback when no cli_path is set, follows the same rule: from .hench/config.json or the hench section of .n-dx.json it applies only once the repository is trusted, and hench.claudePath in .n-dx.local.json always does; ndx config hench.claudePath, hench config claudePath and the dashboard write it there and remove it from .hench/config.json. ndx init writes a Claude binary it finds outside PATH there, and moves one an older init wrote into .hench/config.json to the same place, with or without Claude integration and without overriding a local one; a claudePath you set there by hand stays.

Hench's apiKeyEnv, the variable the API loop sends as the Anthropic key, follows it too. A value other than ANTHROPIC_API_KEY from .hench/config.json or the hench section of .n-dx.json is listed by the trust review and used only once you trust the repository; until then ANTHROPIC_API_KEY is sent. hench.apiKeyEnv in .n-dx.local.json always applies, and that is where ndx config hench.apiKeyEnv, hench config apiKeyEnv and the dashboard write it.

"Always applies" holds while .n-dx.local.json is untracked. A repository that commits the file (git add -f, or a checkout that never ran ndx init) makes it repository config: the trust review lists it (local-config-tracked) with the settings it carries, and nothing in it — guard.env.allow, claudePath, cli.claudePath, cli_path, graview.bin, graview.app, api_endpoint, apiKeyEnv — applies until you trust the checkout. ndx init never probes a cli.claudePath from a tracked file. Outside a git repository nothing is tracked, so the file applies as before.

Effort (API mode) ​

llm.effort sets the reasoning effort per task class, keyed like llm.routes (exact class names or prd.*-style prefixes). Values are low, medium, high, xhigh and max.

json
{ "llm": { "effort": { "prd.*": "low", "agent.execute": "xhigh" } } }

With no matching rule, claude-opus-5-5 gets high (Opus 5's API default; Opus 5.5's own is medium) and every other model gets the API default. A model that does not accept effort, such as claude-haiku-4-5, never receives it, and an unknown value is never sent; both print a warning. CLI mode (Claude Code) does not apply llm.effort yet.

Codex Configuration ​

sh
ndx config llm.codex.cli_path /path/to/codex .
ndx config rex.model gpt-5.6-terra .

TypeSafe Jev for Judgment Calls ​

Some sourcevision calls are judgments, not text generation, and those go to TypeSafe's Jev, which answers typed questions with a probability per option instead of free-text JSON. Setting TYPESAFE_API_KEY opts in; without it every prompt and output is exactly as before.

Task classJudgmentWhat changes
code.classifyChoice over the archetype catalogA file's classification confidence is Jev's probability for that archetype, not a fixed 0.7. A confident none leaves the file unclassified; an unsure answer is escalated to the text classifier with the file's leading doc comment as extra evidence
finding.judgeScore for severity, Choice for category, Nouls for support and metric-paraphrase, Choices for anchor file, type and scopeEnrichment prompts stop asking the text model for severity, category, type and scope. Findings the evidence contradicts are dropped (support below 0.3 — this replaces the hedge-phrase regex), undecided ones (0.3–0.7) are kept with the support probability recorded as their confidence, paraphrases of pass 0 metrics are dropped, Finding.anchors and Finding.confidence are filled. The meta pass re-rates existing findings the same way instead of returning severityUpdates
zone.judgeChoice over deterministic name candidates, Nouls over merge-candidate pairs, two fragility Nouls per zoneZone names are selected from the package name, dominant directories and dominant archetype (a none or spread Choice escalates to three generated names, verified by a Noul); zones the graph links are merged at ≥ 0.8 and reported as an observation between 0.4–0.6; fragility above 0.7 is a structural finding

The cascade ​

With the key present, ndx analyze runs the cascade by default: facts are computed, Jev judges them, and the text model is asked only about what a judgment left uncertain. Fragility probabilities at or above 0.7 become templated findings; a zone whose probability falls in 0.4–0.6 is escalated — the per-zone generative prompt runs for that zone alone (at most six zones per run, highest probability first), and its findings are then judged like any other. Zone descriptions are templated from facts. PRIMER.md is not regenerated in this mode (a cached one is still served).

Generation is one call per kind and off the critical path: the zones that need generated names go to the text model in one prompt, the escalated zones are narrated in one prompt, and analyze returns as soon as the judged results are on disk — narration continues in a detached sv narrate child (log at .sourcevision/.cache/narration.log; manifest.narration is pending until its insights and findings are merged into zones.json, then done or failed with a reason). Pass --wait to narrate before returning (CI), or run sv narrate . by hand to retry.

Only one narrator runs at a time. A new analyze stops a narrator that is still working and queues whatever it had not finished, along with its own escalations; a narrator that died without finishing is re-queued the same way, so a second analyze never drops narration. A failed narration call is not retried automatically. ndx status prints a line while narration is pending or after it failed, and the dashboard overview shows the same. ndx plan passes --wait, because rex analyze reads the zone names.

A re-run with no changes is cache hits and no text-model calls: judged state carries no zone names, a zone whose generated-name fallback already failed on the same files is not asked again, and an escalated zone narrated on the same files last time keeps its insights.

Two more judgments run over the heuristics on every keyed run, whichever mode: each warning-level pass 0 finding is asked whether it is a real problem or a detection artifact (≤ 0.3 demotes it to info, in between records its confidence), and the files with the most cross-zone imports are asked which zone they belong in — a confident answer for another zone becomes a move-file finding with moveReason: "zone-judgment".

--narrate keeps the multi-pass generative machinery (--full, --target-pass, the meta pass); it is the whole pipeline for repositories without a key, so it is not reduced to the cascade's per-zone prompt.

sh
export TYPESAFE_API_KEY=...
ndx analyze .              # cascade: judgments first, narration in the background
ndx analyze --wait .       # cascade, narration before returning
ndx analyze --narrate .    # the full generative pass over every zone, as before the key
ndx analyze --fast .       # no LLM at all

# Send a class back to the vendor tier, or name the route explicitly
ndx config llm.routes.code.classify light .
ndx config llm.routes.finding.judge typesafe .

Thresholds — 0.4 classification probability, 0.5 finding confidence, the 0.3/0.7 support bands, 0.7 paraphrase, 0.8 rescope, 0.8 merge, 0.6 generated-name fit, 0.3/0.7 heuristic bands, 0.7 move, and the 0.4–0.6 escalation band with its cap of six — live in code, not config. enforceSeverityRules still runs last and pass 0 heuristic findings are never re-graded. A class explicitly routed to typesafe without the key prints one notice naming the fallback.

What a re-run does ​

A second ndx analyze on a changed codebase does not start over. Which zones are re-described, and which of the last run's findings are still true, are both judgments over what changed — so Jev decides them, with the hashes kept only as a free pre-filter. A zone whose files and their contents are unchanged is never asked about, so a no-op re-run still costs nothing.

Does this zone need re-describing? For every zone that did move, Jev is shown the previous run's insights beside the delta — files added and removed, whether the file contents changed, whether their leading doc comments changed with them, the change in each boundary crossing, and the cohesion/coupling deltas — and asked whether that alters what a maintainer would say about the zone's purpose, boundaries or risks. At 0.7 or above the zone is re-narrated (it joins the escalation set, ahead of the fragility band and under the same cap of six). At 0.3 or below its description stands untouched. In between it is kept and named in the run record as unconfirmed. Before this, a zone was re-narrated only when its file list changed, so a refactor that moved no files was never revisited.

Does this finding still hold? Each of the previous run's findings that was itself judged — it carries a confidence — is put to Jev against the zone's current files, their leading comments and the same delta. At 0.7 or above it carries forward unchanged; below 0.3 it is dropped as resolved and written to .sourcevision/.cache/rerun-log.jsonl as a finding-resolved entry with its text and probability; in between it carries forward with its confidence lowered to the probability. Pass 0 heuristic findings are recomputed every run as before, and findings from before the judgments existed keep the old rule — any change at all to the zone's content drops them. Before this, one comment edit changed a zone's content hash and dropped every AI finding in it.

Both questions go in one request per batch over one shared state, so a finding references the same zones.<id> evidence the zone's own question does — and when a zone is being re-narrated anyway, any finding Jev could not decide is named in that same narration call to confirm or refute, rather than costing a call of its own.

The counts land on manifest.lastAnalysis.rerun (zonesJudged, refreshed, findingsJudged, carried, resolved, uncertain) and in analyses.jsonl, and the CLI prints one [rerun] line. Without TYPESAFE_API_KEY none of this runs and the hash rules decide exactly as they did before. Thresholds — the 0.3/0.7 refresh bands and the 0.3/0.7 still-holds bands — live in code, not config.

What a run records ​

Partition review. A zone partition is reused when the analysis inputs are unchanged, and otherwise used to seed the next one. Before either happens it is checked. A partition where at least 40% of zones hold two files or fewer is rejected outright. When the signals are only borderline (20% small zones, or a quarter of ids with a numeric suffix like routes-7), Jev is asked one question about the whole map, and a probability of 0.3 or below rejects it. A rejected partition is re-derived from scratch with no stability bias, and only zones with at least 80% file overlap keep their old id and name. The review is stored as zones.partitionReview and manifest.lastAnalysis.partition, and happens at most once per input fingerprint, so a project whose fresh partition is no better does not re-partition on every run. A sourcevision upgrade therefore reaches a stale partition without sv reset. The partitioning algorithm's version is stored as zones.algorithmVersion. A partition from another version is re-derived without stability bias, and names chosen earlier carry over to the new zones.

Route-aware zones. React Router/Remix (react-router.config.*, app/routes.ts), Next (next.config.*) and SvelteKit (svelte.config.*) route roots are detected from the file list and recorded as projectProfile.routeConventions. Route directories (routes, and containers like app/routes/apps/) never name a zone. Each route feature's files are grouped together rather than by the shared components they import, and the route a zone serves is offered as its name.

Balance. A subdivision whose largest child keeps 70% or more of its parent is rejected. It is retried at a finer Louvain resolution, then by directory; if neither balances, the zone keeps no sub-zones. A top-level zone above max(2 × maxZonePercent, 30%) of files without a balanced subdivision makes the partition borderline. Test files in a nested __tests__/tests directory form one zone per parent directory (or per suite for a tests/<suite>/ root). The iso map draws balanced sub-zones as tiles on their parent's top face.

Areas. Above the zones, zones.areas groups them into 4–10 areas. The grouping starts from structure: route-container children like app/routes/apps/* begin together, and in a monorepo each package root is its own area. The rest are merged by import weight, with no area allowed past about 35% of files while another merge is possible. Test zones join the area whose code they mostly import; a suite spread across areas goes to a Tests area. An area whose name would only be a combination of its members is named by Jev, or by the background narrator when no candidate fits. The iso map opens on the areas, with each area's zones as tiles. Double-clicking an area, or Expand here, expands it in place: its zones appear on a platform where the area was, and several areas can be open at once. Zones with tiles on top expand the same way into their sub-zones, as deep as the analysis goes. A zone of 30 or more files is subdivided whenever the split is balanced, and sub-zones are named like zones. Open on its own shows one area by itself, and the URL hash keeps you there across reloads.

Ids. A numbered zone id (routes-7) with a chosen name takes its id from the name. The old id stays in previousIds and still resolves in pins and get_zone.

Every ndx analyze writes manifest.lastAnalysis — mode (fast · generative · narrate · cascade), wall-clock per phase, and calls, tokens and time per LLM task class, so you can see which model answered what and how long it took (a class served by two vendors in one run, such as code.classify with its escalations, appears once per vendor). The same record is appended to .sourcevision/.cache/analyses.jsonl (last 200 runs). Jev answers are cached in .sourcevision/.cache/judgments.json, keyed by the question and only the slice of state it references, so a re-run re-asks only what changed; the CLI's token report shows hits and misses. While a run is in progress it also keeps .sourcevision/.cache/analyze-progress.json current — phase, enrichment pass, batch k of n, and LLM use so far — and marks it complete or failed when it ends; the dashboard reads it for runs started from a terminal as well as its own. These cache files are machine-local and safe to delete.

Hench Configuration ​

sh
ndx config hench.provider api .     # api or cli (api requires claude vendor)
ndx config hench.maxTurns 30 .      # max tool-use turns per task
ndx config hench.maxTokens 100000 . # token budget per task

Configuration is stored in .hench/config.json.

The test gate can be scoped and its flakes absorbed with hench.testGate.command (with {base}) and hench.testGate.rerunCommand (with {suites}). Set them in .n-dx.json; see Completion Waits for the Test Gate.

Review modes ​

hench.review.mode sets how ndx work reviews a task's diff before it is committed.

ModeWhat it does
off (default)No review.
selfThe executing vendor reviews its own work in a fresh session, as --review does.
pairThe other vendor's CLI reviews the diff in a fresh session and reports findings without editing any file.
sh
ndx config hench.review.mode pair .       # off, self or pair
ndx config hench.review.vendor codex .    # the pair reviewer; unset means the other vendor
ndx config hench.review.rounds 2 .        # fix rounds, 1-3 (default 2)
ndx config llm.codex.reviewModel <model> . # the reviewer's model, per vendor

Pair review needs a CLI on both sides: the executor and the reviewer must each be claude or codex running on the CLI provider, and the reviewer must not be the executor's own vendor. Otherwise ndx config and the dashboard refuse the setting.

Findings. Must-fix findings go back to the executor, whose own session is resumed to fix them. The task's tests re-run, and a fresh reviewer checks again, for up to hench.review.rounds rounds. A must-fix finding still open after the last round fails the review. Lesser findings are not fixed: they are captured to the PRD as new items. --review-mode=<off|self|pair>, --reviewer=<claude|codex>, --review, --no-review and a task's saved run.reviewMode / run.reviewVendor / run.review outrank the project setting, in that order; ndx work --task=<id> --resolve . shows which key supplied each value.

Per task. A task can save its own reviewMode and reviewVendor in its run block, so one task is pair-reviewed by a chosen vendor whatever the project default is. A saved value that cannot be honoured is skipped with a warning and never stops a loop: a reviewVendor equal to the executor's vendor falls back to hench.review.vendor, then the executor's other vendor; a reviewMode of pair on an executor with no CLI counterpart, or on the API provider, falls back to the task's run.review, then the project. A reviewer or mode typed on the command line is refused instead.

Web Server ​

sh
ndx config web.port 8080 .         # dashboard port (default: 3117)

Stored in .n-dx.json at the project root.

Feature Toggles ​

Experimental dashboard features are switched in the dashboard's Features settings page or under features in .n-dx.json. All default off.

KeyWhat it doesCost
rex.placementDraftShows Draft with the model in an Inbox change's placement formEach draft is one LLM call on the prd.place model class
json
{ "features": { "rex.placementDraft": true } }

The server enforces the toggle: with it off, the draft endpoint refuses. A draft never writes. It fills the form, and nothing is placed or proposed until you press Place or Propose.

CI: Restricted Imports ​

ndx ci can hold a module to the files allowed to import it — for example, all process spawning through one wrapper. Add rules under ci.restrictedImports in .n-dx.json:

json
{
  "ci": {
    "restrictedImports": [
      { "module": "node:child_process", "allow": ["src/lib/exec.ts", "scripts/release.js"] }
    ]
  }
}
  • module — the import specifier to restrict. child_process and node:child_process are one module.
  • allow — file paths, relative to the project root, that may import it. Omit it to allow none.

The architecture-policy step fails on any other file that imports the module (import … from or require()), naming the file, the module and the rule (ci.restrictedImports[0]). Allow the file by adding it to that rule's allow list. In a git repository the step scans the files git would commit — tracked and untracked, never gitignored — so build output and agent worktrees (.claude/worktrees/) are left out; outside git it walks the directory. Test files and test/ / tests/ directories are exempt, as are node_modules, dist and .git. A rule ndx cannot read — a missing module, an unknown key — fails the step rather than being skipped, and so does a .n-dx.json or .n-dx.local.json that is not valid JSON: the step names the file and the parse error. With no rule configured the step reports skipped.

Viewing Configuration ​

sh
ndx config .              # show all settings
ndx config --json .       # machine-readable output
ndx config --help .       # show all available keys

Configuration Files ​

FileOwnerPurpose
.rex/config.jsonRexPRD configuration, model settings
.hench/config.jsonHenchAgent configuration (provider, max turns, budget)
.sourcevision/manifest.jsonSourceVisionAnalysis metadata and version
.n-dx.jsonn-dxProject-level overrides (LLM vendor/model, web port, etc.)

Assistant Artifacts ​

These files are generated by ndx init and managed by n-dx. They are safe to commit to version control.

FileAssistantPurpose
CLAUDE.mdClaudeProject instructions (auto-generated, re-run ndx init to update)
.claude/skills/*/SKILL.mdClaudeWorkflow skills in YAML frontmatter format
.claude/settings.local.jsonClaudeAuto-approved read-only MCP tool permissions (merged, not overwritten)
AGENTS.mdCodexProject instructions with embedded skill and MCP docs
.agents/skills/*/SKILL.mdCodexWorkflow skills in plain markdown format
.codex/config.tomlCodexMCP server definitions (stdio transport)

See Getting Started — Assistant Surfaces for details on what each artifact contains.

Skills used in this guide ​

SkillSourceRole in this guide
/ndx-config.agents/skills/ndx-config/SKILL.mdInteractive view, explanation, and validation of n-dx configuration settings

For the full skill inventory and customization guidance, see the Skills Reference.

Released under the Elastic License 2.0.