MemtraceDOCS

memtrace rail & routeBeta

Rail is an experimental Memtrace-first discovery router: it intercepts an agent’s raw grep/glob/find calls and, depending on mode, allows, nudges, or replaces them with graph search — fully fail-open by design.

Usage#

memtrace rail [status|report|doctor|enable [mode]|disable|help]Beta

Rail is Memtrace's experimental "Memtrace-first discovery router." Once enabled, it intercepts an AI coding agent's raw discovery calls — Claude Code's native Grep/Glob, or a shell rg/grep/find/ag/fd invocation wrapped in Bash — via PreToolUse-style hooks on supported agent hosts. Depending on the configured mode, it lets the raw search through unchanged, injects Memtrace context alongside it, or suppresses the raw search entirely in favor of Memtrace's own find_code results.

memtrace rail is the management surface: it enables/disables the feature (persisting a mode and wiring hooks into every supported agent host) and reports status/telemetry/wiring health. The actual per-attempt routing decision is made by the sibling memtrace route command, which the installed hooks invoke on every discovery call.

HIDDEN CLI SURFACE

Rail is completely absent from memtrace --help — the only in-repo usage text is memtrace rail help / --help / -h / no-args, printed to stderr. It is off (routing nothing) unless a mode is explicitly enabled or MEMTRACE_RAIL is set.

Modes#

Rail has five modes, resolved with precedence --mode flag on route > MEMTRACE_RAIL env > persisted ~/.memtrace/rail.json > hardcoded default observe.

  • off — routing is a complete no-op; raw search always runs.
  • observe (the global default when nothing is configured) — the hook path never queries the daemon inline. A Memtrace-owned attempt is classified but the decision is always AllowRaw, so agent latency is unaffected. A separate, invisible shadow-measurement spool may record a content-free entry for later trust-dataset aggregation.
  • nudge (the default mode when you run rail enable with no argument) — makes one bounded daemon query per Memtrace-owned attempt. A relevant hit still lets the raw command run, but attaches Memtrace's result alongside it (InjectContext). Never blocks.
  • rail — enforcing. A relevant hit suppresses the raw search entirely and returns Memtrace's result instead (SuppressRawWithResult).
  • strict — same enforcing behavior as rail, the strictest mode currently shipped.
ENFORCING MODES STILL FAIL OPEN

rail and strict never dead-end an agent: any backend outage, timeout, or miss falls back to AllowRaw (or, on a miss, a narrowed raw search still proceeds). The retrieval backend used by the pure decision helper is a hardcoded fail-open placeholder — only the live route --hook path performs a real daemon query.

Subcommands#

status

memtrace rail status is read-only: it prints a JSON object with the resolved mode, the MEMTRACE_RAW_SEARCH escape-hatch state, and a fixed advisory note that routing is search-backed (queries the running daemon's find_code), fails open on any backend failure, and requires memtrace start to be running for routing to take effect. It does not verify the daemon is actually running.

report

memtrace rail report reads the local rich telemetry JSONL (~/.memtrace/rail-telemetry.jsonl) and the content-free shadow telemetry JSONL (~/.memtrace/rail-shadow.jsonl), aggregates each into counters — coverage, precision proxy, hit/miss/unavailable counts, score/latency/query-shape histograms — and prints a combined {"local": ..., "shadow": ...} JSON report to stdout. Missing or unreadable files degrade silently to all-zero counters rather than erroring.

doctor

memtrace rail doctor prints a small fixed JSON health object: router: "ok", the resolved mode, and backend_port: "fail_open_placeholder".

DOCTOR IS A ROUND-1 STUB

It does not actually probe the daemon, hook files, or index — the "placeholder" label documents that the retrieval port is still the fail-open placeholder pending later rounds of the Rail build. Don't expect it to diagnose a broken installation the way memtrace doctor does.

enable [mode]

memtrace rail enable [off|observe|nudge|rail|strict] persists the given mode (default nudge if no argument) to ~/.memtrace/rail.json, then calls the bundled Node installer's install-rail subcommand to wire discovery hooks into every supported agent host: Claude, Cursor, Codex, Gemini, OpenCode, Windsurf, and VS Code Copilot. If the mode is nudge, it prints extra guidance that raw search still runs with Memtrace context injected alongside it.

Hook-wiring requires node on PATH and the bundled installer. If either is missing, enable still persists the mode — it prints a warning and tells you to run the installer manually rather than failing the command.

disable

memtrace rail disable persists mode off and best-effort removes the Rail hook entries from every supported host's config. Unlike enable, a failure here is silently swallowed rather than warned about.

help

memtrace rail help (also the fallback for no args, --help, or -h) prints the Rail usage banner to stderr and exits 0. An unrecognized subcommand instead prints an error to stderr and exits with status 2 — it does not fall back to help.

How a discovery call is routed#

Every wired hook resolves to the same pipeline. Only attempts on a surface Memtrace actually owns (inside the indexed root, source, not generated/vendored) continue past classification — everything else allows the raw command through with zero backend contact.

Discovery call routingFLOW
noyesobserve / offnudge / rail / strictmiss or timeout — fail open
Grep/Glob/rg call
PreToolUse hookmemtrace route
Surface owned?
Mode postureobserve/nudge/rail/strict
Query daemonfind_code, ≤500ms
Allow raw search
TRIGGERSUPPORTINGDECISIONSERVICESUCCESS

In observe mode the daemon is never queried inline — the pipeline stops at the posture check. In nudge/rail/strict, a relevant hit (score clearing a mode-aware floor) either attaches Memtrace's result alongside the raw search (nudge) or suppresses the raw search and returns Memtrace's result instead (rail/strict). Any connection error, non-2xx status, timeout, or bad JSON from the daemon is treated as unavailable and always falls open to allow.

memtrace route#

memtrace route --json '<ToolAttempt JSON>' | --json - | --hook [--mode off|observe|nudge|rail|strict] [--host claude-code|cursor|codex|gemini|open-code|windsurf|vscode|generic]

route is the internal hook entry point that does the actual per-attempt classification. It is invoked once per discovery tool call by an installed PreToolUse-style hook — not typically run by hand. memtrace route ARGS is a thin alias that forwards into the same dispatcher as memtrace rail route ARGS, one token shorter.

Pass --json '<ToolAttempt>' for a one-shot manual classification: it prints a bare DecisionEnvelope JSON to stdout and exits 0. Use --json - or --stdin to read the attempt from stdin instead. The installed hooks actually invoke the --hook form — stdin is a raw host PreToolUse payload (not a pre-built ToolAttempt), and output is reshaped into that host's own hook-output schema (for example Claude's hookSpecificOutput.permissionDecision).

SUB-MILLISECOND BY DESIGN

Both route and rail explicitly skip the normal Rayon thread-pool configuration (and its stderr startup banner) that every other subcommand runs, because the hook path must stay sub-millisecond on the no-op path and must never drive the indexing pipeline.

FlagDescription
--json '<ToolAttempt JSON>'Full ToolAttempt JSON given inline as a single argument.
--json -Read the ToolAttempt JSON from stdin instead of an inline argument.
--stdinEquivalent to --json -.
--hookTreats stdin as a raw host PreToolUse payload and prints host-shaped hook-output JSON instead of a bare DecisionEnvelope. This is the form installed hooks actually invoke.
--repo-id <ID>Explicit cwd-to-indexed-repo id override; wins over filesystem resolution.
--index-version <V>Opaque index-version value passed through as a cache key alongside --repo-id.
--host <name>One of claude-code, cursor, codex, gemini, open-code, windsurf, vscode, generic. Selects the hook-payload adapter for --hook mode; defaults to generic.
--mode <off|observe|nudge|rail|strict>One-shot override of the Rail mode for this single invocation, above the env var and persisted config.

A relevant daemon hit is gated by score, not just result count: the top result must clear a mode-aware relevance floor (0.0 for nudge, since it never blocks anyway; 0.06 for rail/strict, calibrated against the RRF scorer's measured true-hit-versus-junk band) or it downgrades to a miss and the raw command proceeds untouched.

WINDSURF EXITS THE PROCESS DIRECTLY

In --hook mode, every other host prints one line of JSON and returns exit 0. Windsurf is the exception: it calls std::process::exit directly — exit 2 with a stderr message to block the raw command, or exit 0 to allow — matching Windsurf's own hook exit-code contract instead of JSON.

Environment variables#

VariableDefaultDescription
MEMTRACE_RAILobserveOverrides the persisted mode for the current process (off|observe|nudge|rail|strict). Consulted by route/status/doctor mode resolution, not by enable/disable (those always write explicitly).
MEMTRACE_RAW_SEARCHEscape hatch: set to allow (case-insensitive) to force raw search through even in enforcing modes. Also triggered by the literal token raw-search-ok anywhere in the tool input. Surfaced read-only in rail status as env_escape.
MEMTRACE_RAIL_CONFIG$HOME/.memtrace/rail.jsonOverrides the path to the persisted mode file that enable/disable write and status/route read.
MEMTRACE_RAIL_TELEMETRY$HOME/.memtrace/rail-telemetry.jsonlPath to the rich (query-containing) local telemetry JSONL. Rotates to <path>.1 past ~5MB.
MEMTRACE_RAIL_SHADOW_LOG$HOME/.memtrace/rail-shadow.jsonlPath to the content-free shadow telemetry JSONL, kept deliberately separate so the exportable file never carries query content.
MEMTRACE_RAIL_SHADOWonSet to off/0/false/disabled/no to disable observe-mode shadow measurement. Independent of MEMTRACE_TELEMETRY.
MEMTRACE_RAIL_SHADOW_SAMPLE1.0Sampling rate in [0,1] for observe-mode shadow measurement, to bound background daemon CPU.
MEMTRACE_RAIL_SHADOW_SPOOL$HOME/.memtrace/rail-shadow-spool.jsonlTransient spool the hook appends to in observe mode (zero agent latency); drained asynchronously by a daemon background loop every ~20s.
MEMTRACE_TELEMETRYonGlobal telemetry opt-out. When off, also disables Rail shadow measurement.
MEMTRACE_RAIL_SEARCH_URLhttp://127.0.0.1:<MEMTRACE_UI_PORT|3030>Overrides the daemon base URL that route queries for find_code/search results.
MEMTRACE_UI_PORT3030Used to build the default daemon base URL when MEMTRACE_RAIL_SEARCH_URL is not set.
MEMTRACE_RAIL_TIMEOUT_MS500Per-query timeout for the daemon retrieval call, clamped to [1, 5000] ms; invalid values fall back to 500ms.
MEMTRACE_RAIL_MIN_SCOREExplicit relevance-score floor override; must parse as a float in [0,1]. Otherwise mode-aware defaults apply (0.0 nudge, 0.06 rail/strict).
MEMTRACE_INSTALLER_JSUsed by hook-wiring (enable/disable) to locate the bundled Node installer if the default relative-path lookup fails.

Gotchas#

REQUIRES MEMTRACE START RUNNING

Routing only takes effect while memtrace start is running, since route queries it over HTTP on 127.0.0.1:3030 (or MEMTRACE_UI_PORT) with a 500ms default timeout. rail status's own output says this explicitly.

TWO HOSTS HAVE NO RAIL HOOK SURFACE YET

Hermes ("pre_tool_call adapter not wired yet") and Kiro IDE ("hooks omit tool stdin, CLI-only for now") are reported by rail enable as skipped, not registered.

TELEMETRY FILES ARE DELIBERATELY SPLIT

rail-telemetry.jsonl can contain raw query text; rail-shadow.jsonl is content-free by construction so it's safe to aggregate/export as a cross-user trust dataset.

Examples#

terminal
$ memtrace rail status
# Shows resolved mode, MEMTRACE_RAW_SEARCH escape state, and a
# routing-requires-daemon note. Read-only.

$ memtrace rail enable nudge
# Persists mode=nudge and wires discovery hooks into every
# supported host. Requires node on PATH. Never blocks raw search,
# only injects Memtrace context alongside it.

$ memtrace rail enable rail
# Enforcing mode: Memtrace-owned hits suppress the raw search
# result entirely.

$ memtrace rail disable
# Persists mode=off and best-effort removes wired hooks from all hosts.

$ memtrace rail report
# Prints aggregated local + shadow telemetry counters as JSON.

$ memtrace route --json '{"host":"generic","cwd":"/repo","tool":"Bash","input":{"command":"rg foo"},"session_id":"s1"}'
# Manual one-shot classification; prints a DecisionEnvelope JSON.
# In default observe mode this always allows raw and never touches
# the daemon.

$ memtrace route --hook --host claude-code
# The exact form the Claude Code PreToolUse hook invokes: stdin is
# Claude's raw hook payload, stdout is Claude's hookSpecificOutput
# JSON.

See also memtrace start, memtrace mcp, memtrace install, and Hooks.