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]BetaRail 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.
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 enablewith 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.
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".
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.
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).
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.
| Flag | Description |
|---|---|
--json '<ToolAttempt JSON>' | Full ToolAttempt JSON given inline as a single argument. |
--json - | Read the ToolAttempt JSON from stdin instead of an inline argument. |
--stdin | Equivalent to --json -. |
--hook | Treats 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.
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#
| Variable | Default | Description |
|---|---|---|
MEMTRACE_RAIL | observe | Overrides 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_SEARCH | — | Escape 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.json | Overrides the path to the persisted mode file that enable/disable write and status/route read. |
MEMTRACE_RAIL_TELEMETRY | $HOME/.memtrace/rail-telemetry.jsonl | Path to the rich (query-containing) local telemetry JSONL. Rotates to <path>.1 past ~5MB. |
MEMTRACE_RAIL_SHADOW_LOG | $HOME/.memtrace/rail-shadow.jsonl | Path to the content-free shadow telemetry JSONL, kept deliberately separate so the exportable file never carries query content. |
MEMTRACE_RAIL_SHADOW | on | Set to off/0/false/disabled/no to disable observe-mode shadow measurement. Independent of MEMTRACE_TELEMETRY. |
MEMTRACE_RAIL_SHADOW_SAMPLE | 1.0 | Sampling rate in [0,1] for observe-mode shadow measurement, to bound background daemon CPU. |
MEMTRACE_RAIL_SHADOW_SPOOL | $HOME/.memtrace/rail-shadow-spool.jsonl | Transient spool the hook appends to in observe mode (zero agent latency); drained asynchronously by a daemon background loop every ~20s. |
MEMTRACE_TELEMETRY | on | Global telemetry opt-out. When off, also disables Rail shadow measurement. |
MEMTRACE_RAIL_SEARCH_URL | http://127.0.0.1:<MEMTRACE_UI_PORT|3030> | Overrides the daemon base URL that route queries for find_code/search results. |
MEMTRACE_UI_PORT | 3030 | Used to build the default daemon base URL when MEMTRACE_RAIL_SEARCH_URL is not set. |
MEMTRACE_RAIL_TIMEOUT_MS | 500 | Per-query timeout for the daemon retrieval call, clamped to [1, 5000] ms; invalid values fall back to 500ms. |
MEMTRACE_RAIL_MIN_SCORE | — | Explicit 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_JS | — | Used by hook-wiring (enable/disable) to locate the bundled Node installer if the default relative-path lookup fails. |
Gotchas#
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.
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.
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#
$ 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.