MCP · 15. Flag parsing and known drift

Set every Crux environment flag to exactly 1 or 0, with no surrounding whitespace. Six incompatible parsers coexist in the codebase and they disagree on yes, no, on, off, True and any value with a stray space. 1 and 0 are the only values that mean the same thing to all six.

This chapter is a standing disclosure. It lists, with source links, every place we currently know that the Crux repository's own documentation contradicts the Crux code. It is published because you can check it: every claim below carries a file:line link to a public repository, and the commit is named. Nothing here is a hypothetical. Each item states what is wrong, what the evidence is, and what happens to you if you believe the wrong one.

This chapter is explanation and reference. If you only read one section, read 15.1 before you set a flag and 15.2 before you assume an erasure tool is off.

15.1 Six incompatible flag-parsing dialects

There is no shared truthiness helper in the codebase. Six different parsers coexist.

#DialectTreated as ONTrimming and caseRepresentative source
1"not-off", the dominant oneAnything except "", 0, false, off, noTrimmed, lowercasedenvelope.rs:105
1b"not-off" without noAnything except "", 0, false, off. no counts as ONTrimmed, lowercasedreuse.rs:38
2strict allowlistOnly 1, or true case-insensitivelyNo trimmingidentity.rs:66
3env_flag1, true, TRUE, yes, YESTrimmed, case-sensitive against that literal listcontext_custody_audit.rs:41
4bool_value1, true, TRUE, yes, YESNo trimming, case-sensitiveconfig.rs:764, wrapped by env_bool at :738
5env_truthy1, true, yes, onTrimmed, lowercasedagentgraph_kinds.rs:132
6env_default_onAnything except 0 and false. Unset means ONTrimmed, false case-insensitiveconfig.rs:744

Dialect 1 is copied verbatim into at least ten modules: traces.rs:75, ledger.rs:63, memory_use.rs:94, consolidation.rs:54, freshness.rs:59, memory.rs:72, artefacts.rs:60, approvals.rs:118, receipt_verify.rs:52, tools/handoff.rs:198. Dialect 1b is in reuse.rs:38, engrams.rs:36 and autonomy.rs:46. Dialect 2 is in identity.rs:66, audit_export.rs:60 and forget.rs:74.

What each value actually does

This is the whole problem in one table. ON means the flag reads as enabled, OFF means disabled.

Value you set11b23456
1ONONONONONONON
0OFFOFFOFFOFFOFFOFFOFF
trueONONONONONONON
falseOFFOFFOFFOFFOFFOFFOFF
TRUEONONONONONONON
TrueONONONOFFOFFONON
yesONONOFFONONONON
noOFFONOFFOFFOFFOFFON
onONONOFFOFFOFFONON
offOFFOFFOFFOFFOFFOFFON
1 with a leading spaceONONOFFONOFFONON
unsetOFFOFFOFFOFFOFFOFFON

The safe rule: use 1 or 0, and nothing else. Lowercase true and false are also universally correct. Every other value means different things to different flags.

Four consequences an operator will actually hit

  • CORECRUXD_FEATURE_SCOPED_FORGET=yes disables scoped forget. Dialect 2 does not accept yes, and the flag defaults on, so setting it to yes turns off a live GDPR-erasure tool. Setting it to 1 is a no-op. Only 0 was ever needed.
  • FLAG=no disables a dialect-1 flag and enables a dialect-1b flag. CORECRUXD_FEATURE_MEMORY_ACK=no is off; CORECRUXD_FEATURE_REUSE_CHECK=no is on.
  • FLAG=on enables CORECRUXD_ORCHESTRATORS (dialect 5) and reads as OFF for CORECRUXD_FEATURE_IDENTITY_CONTINUITY (dialect 2). The same word, two opposite outcomes, in the same daemon.
  • FLAG=off disables everything except a dialect-6 flag, which it enables. CORECRUXD_COORD=off leaves coordination on. Use CORECRUXD_COORD=0.

Nothing warns you about any of this. There is no validation pass, no startup log line listing parsed flag values, and no test asserting cross-dialect agreement. A misparsed flag is indistinguishable from a correctly parsed one until you notice the behaviour.

Every flag, its default and its dialect

Catalogue-affecting flags; these change what tools/list contains:

Env varDefaultDialectEffect
CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTSoff (config.rs:961)4Presence of request_passport_mint. Dispatch fails closed with -32601 while off
CORECRUXD_AGENT_PASSPORTSoff (config.rs:960)4Tier of issue_passport, hosted to local. The only tier-moving flag
CORECRUXD_TOOL_SURFACEfull (surface.rs:95)enum, not booleanminimal gives the 16-tool floor; dynamic gives floor plus 12. Any unrecognised value falls back to full

Behaviour-gating flags inside crux-mcp, the tool stays in tools/list either way:

Env varDefaultDialectTools gatedBehaviour when off
CORECRUXD_FEATURE_REUSE_CHECKoff1breuse_checkCAPABILITY_DENIED
CORECRUXD_FEATURE_ENGRAM_MCPoff1bengram_resolveCAPABILITY_DENIED
CORECRUXD_FEATURE_AUTONOMY_CONTRACToff1bautonomy_contractfeature_enabled: false, empty capabilities
CORECRUXD_FEATURE_MEMORY_ACKoff1memory_acknowledge_usedisabled response
CORECRUXD_FEATURE_C2PA_OUTPUToff1output_attestdisabled
CORECRUXD_FEATURE_C2PA_X509_SIGNERoff1output_attest signer mode-
CORECRUXD_FEATURE_ARTEFACTSoff1artefact_put, artefact_get, artefact_listdisabled
CORECRUXD_FEATURE_RECEIPT_VERIFYoff1receipt_verifyerrors: ["FEATURE_DISABLED"]
CORECRUXD_FEATURE_AUDIT_EXPORToff2audit_export_bundledisabled
CORECRUXD_FEATURE_IDENTITY_CONTINUITYoff2passport_split, passport_merge, passport_link_device-32601
CORECRUXD_FEATURE_APPROVAL_QUEUEoff1approval_request, approval_decidestatus: 'feature_disabled'
CORECRUXD_FEATURE_AUDIT_ENVELOPEoff1Not a tool. Wraps structuredContent.envelope on query_facts, memory_acknowledge_use, memory_freshness, artefact_listplain payload shape
CORECRUXD_FEATURE_TOOL_LEDGERoff1Cross-cutting agent.tool_invocation.v1 and agent.tools_offered.v1no emission
CORECRUXD_TOOL_LEDGER_RAW_ARGSoff1Ledger argument redactionargs redacted
CORECRUXD_FEATURE_OTEL_SPANSoff1Span emissionno spans
CRUX_CONTEXT_CUSTODY_AUDIToff3context_custody_audit{enabled: false, note}
CRUX_OUTPUT_HOLDOUT0.0, offfloat, clamped 0 to 1token_savings; also forces the unshaped path in query and query_factsdisabled stub
CORECRUXD_FEATURE_TOOL_TRACESon (traces.rs:80)1tool_trace_recent, learn; feeds dynamic surface scoring{traces: [], count: 0, feature_disabled: true}
CORECRUXD_FEATURE_TOOL_TRACES_TTL_SECS3600integerTrace retention-
CORECRUXD_FEATURE_MEMORY_PANELon (memory.rs:78)1memory_view, memory_edit, memory_pin, memory_historydisabled
CORECRUXD_FEATURE_FRESHNESSon (freshness.rs:65)1memory_freshness, memory_sweep_candidates, memory_reverify, memory_set_horizonCAPABILITY_DENIED
CORECRUXD_FEATURE_CONSOLIDATIONon (consolidation.rs:60)1memory_contradictions, memory_consolidatedisabled
CORECRUXD_FEATURE_SCOPED_FORGETon (forget.rs:76)2memory_forget only; the dry run is always availabledisabled
CORECRUXD_FORGET_RECOVERY_WINDOW_DAYS7, minimum 1integerForget recovery window-
CORECRUXD_MEMORY_SALIENCEoff-Recall-salience recording inside query_factsno recording
CORECRUXD_SESSION_TOKEN_BUDGETunset or 0 means no limitintegersession_token_usage reporting onlyreport-only
CORECRUXD_SYNC_ENABLED, _REMOTE_URL, _API_KEYunset-sync_pull, sync_push, sync_statussync not configured message
CORECRUXD_AUDIT_EXPORT_DIRunsetpathBundle output directory-
CORECRUXD_APPROVALS_SLACK_WEBHOOK_URLunsetpresenceApproval notificationno notify
CORECRUXD_HANDOFF_OBSERVATIONSoff1Handoff observation emission-
CRUX_PASSPORT_REVOCATIONON when unsetonly 1 / true keep it onRevocation refusal on dispatch (MCP plane only)any other set value, yes, on, enabled, silently disables it

Correction, 2026-07-27. An earlier version of the row above stated CRUX_PASSPORT_REVOCATION as defaulting off and failing open. That was wrong, and wrong in the direction that flattered us, the daemon is safer than this chapter claimed. revocation_enforced_from_env() ends in .unwrap_or(true) (dispatch.rs:118), and the doc comment above it reads "default ON (proven live)". The real hazard is narrower and still worth knowing: once the variable is set, only 1 or a case-insensitive true keep enforcement on, so a plausible-looking CRUX_PASSPORT_REVOCATION=yes turns a security control off. Enforcement is also MCP-plane only, with no HTTP equivalent.

It is recorded here rather than silently edited because a chapter that publishes other people's drift owes the reader its own.

CRUX_MCP_HANDOFF_SECRETunsetpresenceRestart-durable handoff signingin-memory only
CRUX_AGENT_TOKEN, CRUX_AGENT_TOKENSunset-Agent auth registryanonymous allowed
CRUX_AGENT_PASSPORTSunset means the built-in map-Agent-to-passport mapping, only read when CORECRUXD_AGENT_PASSPORTS is on-

Daemon-side flags that gate loopback tools, the MCP tool stays listed and returns a disabled notice or a 404-mapped message:

Env varDefaultDialectMCP tools affected
CORECRUXD_COORDon (config.rs:1336)4 with a default-true wrapper; only explicit 0 or false disablescoord_status, coord_announce
CORECRUXD_COORD_PRESENCE_TTL_SECSsee config.rs:1337integerPresence window in coord_status
CRUX_EXECPLANS_ROOTunset means no ExecPlan itemspath presencelist_work ExecPlan projection
CORECRUXD_FEATURE_ACTIVITY_LOGoff (activity.rs:58)1activity_recent
CORECRUXD_FEATURE_ACTIVITY_LOG_TTL_SECS365 daysintegerRetention
CORECRUXD_FEATURE_STATUS_FEEDoff (status_feed.rs:40)1status_feed
CORECRUXD_ORCHESTRATORSoff (agentgraph_kinds.rs:144)5All five orchestrator tools
CORECRUXD_PUNCHCARDoff; mode enum advisory or enforce (agentgraph_kinds.rs:159)enumAll five punchcard tools. enforce is what makes check_punchcard blocking
CORECRUXD_AGENTGRAPH-5Route-auth class for orchestrators and punchcards (route_auth.rs:527)
CORECRUXD_OBSERVEoff (agentgraph_kinds.rs:139)5list_observations, get_observation, verify_observation
CORECRUXD_INTEGRATIONS_ENABLEDon (config.rs:1377)default-onGates the GitHub integration routes that populate the corpus the github_* tools read

15.2 memory_forget misdescribes its own flag default

This is a correctness bug in the product's own self-description, on a GDPR Article 17 erasure tool.

memory_forget's catalogue description (mod.rs:484) says the tool:

Requires authenticated agent identity and feature flag CORECRUXD_FEATURE_SCOPED_FORGET=1.

That reads as "default off; set the flag to enable". The code defaults it ON (forget.rs:69):

fn feature_enabled() -> bool {
    // Launch default ON, scoped forget (GDPR Art. 17 surface) is available
    // out of the box. […] Explicit `CORECRUXD_FEATURE_SCOPED_FORGET=0`
    // disables the mutating tool.
    env::var(FEATURE_FLAG_ENV)
        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
        .unwrap_or(true)
}

The consequence. An operator reads the tool description, concludes that a memory-erasure tool is disabled by default on their deployment, and does not disable it. It is live. The description is not merely stale prose; it is served to every agent in tools/list, so an agent reasoning about what it is permitted to do reads the same wrong statement.

It compounds with 15.1. The flag uses dialect 2. An operator who does want it off and reaches for CORECRUXD_FEATURE_SCOPED_FORGET=no gets the correct outcome by accident; one who reaches for =off also gets it; one who reaches for =yes intending to enable it silently disables it. The only value that reads unambiguously is 0.

memory_forget_dry_run is unaffected; it is deliberately not flag-gated (mod.rs:518) and is always available.

15.3 Eight high-severity drift findings

These are places where an agent or an operator following the Crux repository's own documentation gets a wrong result. Four fail silently, no error is returned, and the wrong answer is indistinguishable from a right one.

#The driftFails silently?DocumentationCode
1query_facts(entity_prefix=…, key=…) is not a thing. Both arguments are dropped. An unfiltered top-10 comes back and reads like a filtered answer. The whole retrieval recipe at agent-guide.md:322-348 depends on it, and the doc calls that recipe "the integrity gate"Yesdocs/agent-guide.md:345Schema mod.rs:326; the handler hardcodes entity_prefix: None at facts.rs:487; there is no key argument
2GET /v1/gaps does not exist. It appears inside the "coverage below 0.5" playbook. The real surfaces are the MCP tool get_gaps or GET /v1/features/capabilities/analysis/gapsNo, 404docs/agent-guide.md:501http/mod.rs:741; tool at mod.rs:1023
3get_bootstrap(…, token_budget=500); there is no such parameter. It is step 1 of the documented "Your First 3 Calls", and the convention sentence reinforces itYesdocs/agent-guide.md:19, :98mod.rs:378; the handler hardcodes top_k: 100, token_budget: None at facts.rs:844
4CORECRUXD_SEED_BOOTSTRAP=0 does not exist anywhere in the codebase. The variable appears in no crate, no script and no example env file, only in this doc and the file generated from it. An operator who sets it believes bootstrap seeding is disabled. Seeding is unconditionalYesdocs/agent-guide.md:74Seeding is unconditional at main.rs:992
5/console/codegraph is not a route. The same claim is repeated in AGENTS.md:27 and CHANGELOG.md:56. The console serves only /console, /console-assets/{name}, /console-v2/{name} and /console-3d/{*path}No, 404docs/mcp-system-prompt.md:120console.rs:301
6list_sessions has no prefix filter. The claim sits inside the documented crash-recovery procedure, which therefore degrades to "list everything" at the moment you most need a narrow answerYesdocs/mcp-system-prompt.md:72mod.rs:909, include_archived is the only parameter
7A broken cross-reference. agent-guide.md tells the reader that mcp-system-prompt.md catalogues declare_constraint, check_constraints, issue_passport, .cruxpack and session_token_usage. All five appear zero times in that fileNo, the reader finds nothingdocs/agent-guide.md:34-38Absent from docs/mcp-system-prompt.md
8memory_forget's own description states the wrong flag default for a live erasure tool. Full detail in 15.2Yes, in the sense that the operator's belief is wrong and nothing corrects itmod.rs:484forget.rs:76

Medium-severity drift

#The driftDocumentationCode
9The prompt file covers 35 of 119 tools while two index files bill it as the full surface. See 15.4AGENTS.md:31, llms.txt:27-
10"Always pass token_budget (500 default)", no such default exists on the retrieval or fact tools. 500 is a default only on memory_freshness, memory_sweep_candidates and memory_contradictions. It is stated as a code default; it is a conventiondocs/mcp-system-prompt.md:122, docs/agent-guide.md:29-30freshness.rs:105, consolidation.rs:96; and see 11.11
11Bootstrap seeding is described as happening "only when the store has no existing __bootstrap__:: facts". The code also backfills newly embedded facts into already-seeded storesdocs/agent-guide.md:72-74bootstrap.rs:107
12Line anchors in docs/developer-guide/10-mcp-surface.md have drifted by a few lines in three places. Otherwise that document is accurate, and its ground-truth block is the model the rest of the docs should followas citedas cited

Low-severity drift

#The drift
13docs/agent-guide.md:139 has a heading "Unified login (crux login)". There is no crux binary; the CLI is corecruxctl. The next line self-corrects
14docs/agent-guide.md:284 claims "about 12 tokens" per fact and :519 claims "60-80%". Neither carries a code reference or a benchmark corpus, so neither is quotable
15docs/mcp-system-prompt.md:85 correctly documents punch_in on tree://<dir>, but punch_in's own schema example (mod.rs:2361) shows only file://. Here the documentation is right and the code's schema example is the inconsistent one

15.4 49 of 119 tools are undocumented in the repository

Zero occurrences across docs/**/*.md, README.md, AGENTS.md and llms.txt:

approval_decide, approval_request, archive_session, artefact_list, attach_to_orchestrator, audit_config, audit_export_bundle, auth_posture_audit, autonomy_contract, check_config_audit, check_punchcard, comment_on_work, context_custody_audit, create_orchestrator, create_work, delete_session, detach_from_orchestrator, egress_policy_check, engram_resolve, enrich_action, execplan_gate, force_release, get_project_context, get_workspace_storyline, github_comments_since, github_open_issues, github_open_prs, github_recent_commits, github_search, list_entities, list_orchestrators, list_projects, list_punchcards, list_work, memory_acknowledge_use, memory_history, memory_set_horizon, memory_sweep_candidates, passport_merge, passport_split, punch_out, reuse_check, route_access_matrix, status_feed, sync_pull, sync_push, unarchive_session, update_orchestrator, update_work_state.

Entire planes are invisible: the work board, the orchestrators, the GitHub corpus, the human-in-the-loop approvals, and sync_push and sync_pull, the last while docs/agent-guide.md devotes a whole section to sync without ever naming its two tools.

Meanwhile docs/mcp-system-prompt.md names 35 of 119 tools, and AGENTS.md:31 sends agents there for "the full tool surface" while llms.txt:27 calls it a "drop-in prompt covering the full tool surface". Both over-claim. Chapters 12 and 13 of this set are the first complete published description of all 118.

Two things worth recording on the other side of the ledger, because they are the parts that are healthy:

  • Zero doc-mentioned tool names are dead. All 41 tool names appearing across the two agent-facing docs resolve against the code. No rename has orphaned a reference. The problem is omission, not rot.
  • The output contract is complete. tool_output_docs() (mod.rs:2728) covers all 119 tools and is CI-guarded by tool_output_docs_covers_all_tools (mod.rs:3270). A tool cannot ship without an output contract. It can ship without prose.

15.5 Why the CI check does not close this gap

The repository has an agent-docs freshness check, .github/workflows/agent-docs.yml driving scripts/check-agent-docs.sh, configured by the ci_assertions: block in docs/agent/repo-manifest.yaml. It cannot catch a single item in 15.3. Being precise about why matters more than claiming coverage.

What it does assert:

AssertionLine in the script
Crate directories exist:71
60 named Rust symbols have a definition site:78
Named test functions exist:89
Fuzz targets exist:95
A fixed list of docs exists:101
llms.txt local-link parity:114
Per-crate AGENTS.md is 60 lines or fewer:129
llms-full.txt freshness:145
cargo fmt --check:154

What it asserts nothing about: tool counts, tool names in prose, parameter names, environment-variable names, HTTP routes, or ports.

Of the 60 named Rust symbols it does check, exactly two are MCP tool handlers, handle_issue_passport and handle_declare_constraint (repo-manifest.yaml:192-193). The check confirms those two functions exist. It does not confirm that anything written about them is true.

And the one doc-touching gate is a closed loop. The llms-full.txt freshness check regenerates llms-full.txt from agent-guide.md. A wrong claim in the guide propagates cleanly into the generated file, the diff is empty, and the check stays green. Both of the stale claims in items 3 and 4 above are visible verbatim in llms-full.txt at lines 1986 and 2019, the generated artefact faithfully reproduces the error it was generated from. A freshness check against a stale source measures freshness, not truth.

What would close it

Three changes, in increasing order of cost, stated so the gap is legible rather than merely admitted:

  1. Tool-name set equality. Add an mcp_tools: list to repo-manifest.yaml and assert set-equality between the backticked tool tokens in docs/mcp-system-prompt.md and the output of list_tools_with_flags. Catches "a doc names a dead tool" and "the code has an undocumented tool" immediately, items 9 and the whole of 15.4.
  2. Route existence. Every GET/POST/PUT /v1/... literal in docs/**/*.md must appear in the daemon's RouteEntry table, which is already parity-tested against the mounted router (route_spec_drift.rs:757). Catches items 2 and 5.
  3. Generate the tool reference. Make tool_output_docs() the generator for a committed docs/mcp-tools.md and diff it in CI. That would make 49 undocumented tools structurally impossible.

None of these is implemented today. That is the honest status.

15.6 Other standing risks in the MCP surface

Not documentation drift, but the same category of thing a reader is entitled to know before depending on the surface. Each is described in full where it lives.

RiskWhere it is described
The three core retrieval tools return unbounded results when token_budget is omitted, and three tool descriptions claim a mandatory budget that no code enforces11.11
TOOL_SURFACE is a 36-entry partial table against a 118-tool catalogue with no compile-time exhaustiveness check. A newly added hosted tool defaults to local silently14.6
Hosted-tier enforcement fails open when no RCX router is wired. corecruxd always wires one; embedders of crux-mcp may not14.5.3
proof_verify is a dead entry in the affinity table and is not a tool; the journal and economy affinities have no tools. Unlike the core floor, the affinity table has no existence test11.10
Extension tool names are not validated for the ext. prefix, and a collision with a built-in name yields a duplicate tools/list entry that dispatches to the built-in11.9
The OAuth read-only allowlist permits four methods that do not exist, ping and three resources/* methods11.4
Ten tools are advertised but return 501, five orchestrator and three punchcard scaffolds, documented only inside the output-contract document13.14, 13.15

Sources

All line references were verified at commit 93b41a7. This chapter is reviewed whenever the audit behind it is re-run; if you find an item here that has since been fixed, the fix is the news, not the disclosure.