Status: Proposed. No code exists. This record decides the shape; §3 lists the tests the implementation lane owes and §8 the one measurement owed before Accepted. Nothing here is implemented, and the flag it names does not yet parse.
Where the addresses point. Written before its code, against 9590682 — the
main this branch left. Every file:line below was read at that commit and
resolves exactly there; the Context section is ABOUT that tree. The Decision
section describes code that does not exist yet, so its addresses are the call
sites to change, given as file, line and function name — the line will go
stale the moment the diff lands, the function name will not.
Date: 2026-08-21
toolPolicyFor (internal/coordinator/coordinator.go:691) is one planned
node's whole execution ceiling. Layer 1 is SettingSources, and its value is
isolatedSettingSources() — a pointer to ""
(internal/coordinator/coordinator.go:708-711). The function's own comment
names the price without being asked to, at
internal/coordinator/coordinator.go:687-690:
The cost, which belongs in the README and not in a surprise: a planned node also loses the user's CLAUDE.md, hooks and MCP servers. Planned nodes are more isolated and less capable than they were. That is the intended direction.
That comment is the whole of this ADR's subject. The direction is not disputed here and is not reversed here; what is decided is whether the operator may depart from it deliberately, for one run, out loud.
Two more facts from the same function, both load-bearing below:
- Enterprise policy is not in the trade.
internal/coordinator/coordinator.go:705-707: "Enterprise policy settings and--settingsflag settings are unioned on top and cannot be dropped by this flag, so it can never be used to step around a corporate policy." Layer 1 could never drop managed policy, and nothing in §2 can grant it either — the mechanism decided below is the ABSENCE of an argument, and an absent argument cannot subtract from a union it never joined. - Layer 4 is a separate axis.
StrictMCPConfig: trueis set atinternal/coordinator/coordinator.go:696, next to layer 1 but independent of it. This matters in §2.3: turning layer 1 off alone would leave--strict-mcp-configon the argv, and any disclosure claiming "your MCP servers load" would then be a sentence nobody had measured. DESIGN.md:2919-2923 is explicit that E5 was never measured and that "no claim is made" that the flag closes MCP.
The policy is runtime-blind; each protocol renders it.
Codex — internal/runner/codex_protocol.go:28-48. When
spec.Policy.SettingSources != nil, buildArgs appends exactly four things
(:37-43):
if spec.Policy.SettingSources != nil {
args = append(args,
"--ignore-user-config",
"--ignore-rules",
"--config", "project_doc_max_bytes=0",
"--config", "mcp_servers={}",
)
}The VALUE is never read — only its nil-ness. --sandbox and
approval_policy="never" are appended unconditionally above it
(:29-35), outside that branch.
Claude — internal/runner/claude_protocol.go:30-67. The Claude equivalent
of those four flags is one flag, and its exact spelling is
--setting-sources "" (:40-42):
if policy.SettingSources != nil {
args = append(args, "--setting-sources", *policy.SettingSources)
}Here the value IS read. One flag on one side, four on the other, from one nil check — which is why §2.2's symmetry question cannot be answered by convenience: both sides already hang off the same bit.
The claim was checked end to end rather than taken.
internal/schedule/scheduler.go:1360-1369, (*Scheduler).policyFor:
if s.toolPolicies == nil {
return runner.ToolPolicy{AllowedTools: node.AllowedTools}, nil
}A nil map is the hand-written run path, stated at
internal/schedule/scheduler.go:1346-1349. The returned policy carries
AllowedTools and nothing else, so SettingSources is nil, Tools is nil,
StrictMCPConfig is false and DisallowedTools is empty. Options.ToolPolicies
is populated at exactly three sites, all of them auto/resume
(cmd/oh-my-graph/main.go:922, cmd/oh-my-graph/resume.go:543 and :607);
the run path passes none. The behaviour is already pinned by
TestScheduler_HandWrittenGraphGetsNoCeiling
(internal/schedule/scheduler_test.go:1679-1710), whose assertions include:
hand-written node got settings isolation %q; its own settings are the intended policy
The claim is true. A hand-written graph run with run already carries the
operator's settings, CLAUDE.md, hooks and MCP servers, on both runtimes, today,
with no flag. That is not a gap to be closed; it is the reference behaviour this
ADR makes reachable from auto.
coordinatorInvocation (internal/coordinator/coordinator.go:357-363) builds
the PLANNER's stance:
Policy: runner.ToolPolicy{DisallowedTools: disallowedToolsFor(graph.Node{})},SettingSources is nil. internal/coordinator/assess.go:186-189 says why in
so many words — the planner's "job includes reading this repository's
CLAUDE.md". So in every auto run today, one of the two kinds of call already
runs under the operator's full configuration and the other does not. The
ceiling is a planned-node ceiling, not a run ceiling, and this ADR does not
widen it to a place it never reached. (The ASSESSOR is the opposite case and
stays that way: assess.go:199-206 gives it layer 1 because its input is
untrusted model output by design. §2.8.)
Both live on the plan screen printed by printPlanForRuntime
(cmd/oh-my-graph/main.go:1096-1137) — after planning, before any node spends,
two-space indented, reached by auto, auto --plan-only, every later cycle of
a goal loop (cmd/oh-my-graph/goal.go:101) and chat's [y/N].
-
Claude:
noteCeiling(cmd/oh-my-graph/main.go:1597-1612), called at:1126. It prints, unconditionally for every planned run:Planned nodes run isolated: none of your user/project/local settings load, so a declared scope like Bash(git *) is enforced rather than merely requested — and your CLAUDE.md, hooks and MCP servers are unavailable to them. See SECURITY.md for what this does not cover. -
Codex:
noteCodexRuntimePolicy(cmd/oh-my-graph/main.go:1152-1182), called at:1122withisolated=true. Its last line, guarded by that parameter (:1179-1181):Auto-planned Codex nodes also ignore user configuration, repository rules, project instructions, and MCP servers.The same function is called with
isolated=falsefrom therunpath (cmd/oh-my-graph/main.go:352),--dry-run(cmd/oh-my-graph/dryrun.go:58) andlint(cmd/oh-my-graph/lint.go:94) — so the parameter is ALREADY the "is layer 1 on" carrier, and already tells the truth about a hand-written graph.
The --verify-cmd advice is the OTHER seam, and it is a different one:
coordinator.VerifyAdvice (internal/coordinator/verifycmd.go:504-540) prints
unindented, BEFORE the planner call, because the flag it names must be typed at
launch. Its plan-screen half is noteMissingBuildEvidence
(cmd/oh-my-graph/verifycmd.go:209-233) — a note* sibling that ADR 0030 added
to printPlanForRuntime rather than opening a channel of its own. That is the
precedent §2.6 follows.
noteCodexRuntimePolicy's own comment (cmd/oh-my-graph/main.go:1149-1151)
sets the budget this ADR must live inside:
Deliberately one line per difference and no more. A disclosure long enough to scroll past is one nobody reads.
The honest alternative was argued first, and it is real. It is also already
documented, at cmd/oh-my-graph/main.go:1010-1014:
saveGeneratedSpecpersists the planner's JSON spec into dir — the run's own directory for a run,plans/<id>for a--plan-onlypreview — so an auto plan stays inspectable and repeatable: JSON is valid YAML, so the saved file can be hand-edited and re-run directly withoh-my-graph run <path>.
So an operator who wants their own configuration can, today, with no new code:
auto --plan-only, read the plan, then run plans/<id>/graph.json. By §1.3
that second command carries their settings, CLAUDE.md, hooks and MCP. They do
not even lose the planning — the plan is bought and saved.
That path is not deprecated by this ADR and does not change. But it is not a substitute, for two reasons, and the second is decisive:
- The goal loop is out of reach.
runexecutes one graph once.auto's plan → execute → assess → re-plan cycle (--max-cycles,cmd/oh-my-graph/goal.go) re-plans from the assessment of the previous cycle, and there is no two-step equivalent: a human cannot hand-carry cycle k's assessment into cycle k+1. An operator whose nodes need their MCP servers therefore cannot use the goal loop at all. - The workaround drops the ENTIRE ceiling, not just layer 1. By §1.3, a
node run through
rungets no--toolsnarrowing, no--disallowedToolsand no binding--allowedTools. The flag decided below drops layers 1 and 4 and keeps layers 2, 3 and 5. So refusing to build it does not preserve the ceiling — it routes every operator who wants their config through the one door that removes all five layers, and removes the plan screen's disclosure with them, because a hand-written graph is by definition the user's own reviewed artifact and is disclosed as such.
Choosing NO FLAG would mean choosing the wider escape hatch on the grounds that it is the one that already exists. A flag ships. Its justification is not capability; it is that the narrower door should exist beside the wider one, and should say what it costs.
One flag, for the whole run, on auto only. Not per node: a per-node
opt-in would have to come from the plan, and the plan is the planner's
untrusted output. That is the hole validatePlannedNodeAgent closes —
internal/coordinator/coordinator.go:726-729 names it exactly, "a planner that
could name Skill in allowed_tools would be a planner selecting which of the
user's local files loads into a node it authored" — and it stays closed. A
planner that could ask for the operator's settings would be worse than one that
could ask for a skill.
toolPolicyFor isolates settings for Claude and Codex alike; the price named at
coordinator.go:687-690 is paid on both. A Codex-only escape hatch would be a
claim that the ceiling is worth less on Codex than on Claude — a claim no
measurement in this repository supports, and one that would be made silently,
by a flag's scope, rather than argued.
Three further reasons, in order of weight:
- The mechanism is one bit, already shared. §1.2: both protocols branch on
SettingSources != nil. A runtime-neutral opt-in is the absence of a pointer. A Codex-only one needs a runtime branch in the coordinator, which would be the first thing intoolPolicyForthat knows which CLI it is building for — and this project's most load-bearing policy is deliberatelyONE list, NO runtime branch(internal/childenv/childenv.go:1-25). The cheap thing and the right thing coincide here, which is exactly the case in which it is worth writing down that the cheapness was not the reason. - The operator's motive is runtime-independent. Wanting one's own CLAUDE.md/AGENTS.md, hooks and MCP servers is not a fact about which CLI is installed.
- The CONSEQUENCES differ, and that is a disclosure problem, not a scope
problem. On Codex the sandbox floor survives (§1.2:
--sandboxandapproval_policy="never"sit outside the branch). On Claude, restored settings restore the operator's standing permission grants, andcoordinator.go:672-680records what that does: a standingBash(*)in~/.claude/settings.jsonmatches before the node's narrowerBash(git *), so layer 2 goes back to being a declaration rather than a limit. Same flag, different bill. §2.6 therefore prints a different sentence per runtime — and that is the correct place for the asymmetry, because the asymmetry is in what is true, not in who may ask.
With the flag on, toolPolicyFor builds:
| layer | field | default | with the opt-in |
|---|---|---|---|
| 1 isolation | SettingSources |
&"" |
nil |
| 2 grant | AllowedTools |
node's declaration | unchanged |
| 3 narrowing | Tools |
narrowedToolsFor(node, …) |
unchanged |
| 4 MCP | StrictMCPConfig |
true |
false |
| 5 residual | DisallowedTools |
disallowedToolsFor(node) |
unchanged |
Layer 4 moves with layer 1 and this is not a detail. §1.1: --strict-mcp-config
is an independent axis, and DESIGN.md:2919-2923 says nobody has measured whether
it closes MCP. If it were left on, the disclosure in §2.6 would be asserting
something unmeasured; if it were left on and it DOES close MCP, the disclosure
would be false. Dropping it makes the sentence true by construction. The
resulting rule is one line and testable:
An opted-in planned node's setting/config posture is exactly a hand-written
runnode's, and its tool posture is exactly a planned node's.
The residual layers are worth keeping and are not decoration: --tools
replaces the built-in tool set outright and is independent of settings (E4,
DESIGN.md:2911-2918 — "a tool left out of --tools is absent even when
--allowedTools names it"), and --disallowedTools is subtractive. Both
therefore still bind under restored settings. That is precisely what makes this
door narrower than §2.1's workaround, and it is the claim §8 requires measured
before this ADR is Accepted.
Both mechanisms rest on layer 1 being "". Measurement (j)
(docs/measurements/0017-lifting-the-agent-mapped-exclusion.md:110,119) is
direct, and the two arms differ only in that field:
- arm
X-ACT— activated node's argv,--setting-sources "", under a three-way name collision: resolved to the STAGED copy, 3 of 3. - arm
X— same fixture,SettingSources = nil: resolved to the REPO copy, 3 of 3.
So under the opt-in a same-named .claude/skills definition in the working
repository shadows the corpus oh-my-graph staged, and the same reasoning reaches
--agent: ADR 0022's guarantee is that a mapped node's system prompt comes from
a directory oh-my-graph staged rather than from discovery, and that guarantee is
held by layer 1.
Rather than let those two guarantees become quietly false, WithLoadedUserConfig()
sets agentMappingOff and skillActivationOff itself — in the Option, not at
the call site, so no future composition of options can reopen it. The CLI prints
the reason where it prints the other de-escalations.
This is a change of provider, not a loss: with the operator's settings loaded,
the selected CLI discovers the operator's own agents and skills natively, which
is what a hand-written run node has always done. What is given up is the
staged copy's attributable name and its shadow-proofness — stated, not
absorbed.
No value. Default OFF — the ceiling stays the default, and a run that types nothing is byte-for-byte the run that shipped in v0.10.0.
ADR 0030 §2.3 is the precedent and it is a rule about grammar: a flag names what the operator IS ACCEPTING, in a sentence they say, not a knob they turn. Candidates were judged on what typing it asserts:
| candidate | what typing it asserts | verdict |
|---|---|---|
--allow-user-config |
"permit the config plumbing" | rejected. It describes the mechanism (SettingSources) from the engine's side and grants a permission to the machine, not a statement by the operator. It is also silent about the bill — an operator reads it as pure capability, which is the one reading §2.6 exists to prevent. Precedent rejects exactly this shape. |
--no-config-isolation |
ambiguous — a switch or a statement | rejected. It joins the --no-agent-mapping / --no-skill-activation family, which are genuine feature switches and are all DE-escalations. This flag is the only one in auto that widens; family resemblance is precisely the wrong signal. It also names what is skipped rather than what the run then carries. |
--accept-standing-grants |
"I accept that my standing permission grants apply" | close, and rejected on discoverability. It names the sharpest half of the bill (§2.2(3)) and nothing of what the operator came for; nobody looking for "how do my MCP servers reach a planned node" would ever find it, and on Codex, where the grants story is different, it barely parses. |
--accept-loaded-user-config |
"I accept that this run's planned nodes load my user configuration" | chosen. Subject, verb, object. loaded states what the run now carries and is the one word that distinguishes it from today; accept marks it as a bill the operator signs, not a feature they enable; user-config is the term both CLIs use for the thing, and is what an operator would search for. It reads correctly in the three places it will be read — on the command line, on the plan screen, and in a CHANGELOG line skimmed a year from now. |
It is long on purpose, and being annoying to type once per run is the design working. No name prevents a shell alias; only the disclosure in §2.6 and the record in §2.7 make the choice visible after the fact.
No flag-vs-flag refusal. Unlike ADR 0030 §2.3's contradiction with
--verify-cmd, this flag contradicts nothing an operator can also type:
--no-agent-mapping and --no-skill-activation are already implied by it
(§2.4) and passing them alongside is redundant, not inconsistent. Refusing a
redundancy would cost a real script for no gain.
Help text (one line, cmd/oh-my-graph/flags.go, newAutoFlags):
state that this run's planned nodes load YOUR CLI configuration, and run anyway (ADR 0032):
user/project/local settings on Claude, ~/.codex/config.toml plus repository rules and AGENTS.md
on Codex, and with them your CLAUDE.md, your hooks and your MCP servers. This is not only a
capability — your standing permission grants load too, so on Claude a node's declared scope like
Bash(git *) stops being enforced and is a declaration again; each node's --tools set and deny list
still bind, and enterprise/managed policy is unaffected and cannot be widened by this flag. Agent
mapping and skill activation are turned OFF for the run, because a staged definition is shadowed by
a same-named one your restored settings discover. The choice is printed with the plan and readable
in this run's state.json
Printed on the plan screen of §1.5 — the seam that already exists — through a
note* sibling of noteCeiling and noteMissingBuildEvidence, called from
printPlanForRuntime. No second channel: not stderr, not a pre-planner line,
not a new writer. One slot, which says the isolated sentence or the loaded one
and never both and never neither, exactly as ADR 0030 §2.5b requires of the
build-evidence slot.
Claude, replacing noteCeiling's paragraph for the run that opted in:
Planned nodes run with YOUR configuration (--accept-loaded-user-config): your user, project
and local settings load, and with them your CLAUDE.md, your hooks and your MCP servers.
Your standing permission grants load too, so a declared scope like Bash(git *) is a
declaration again and not a limit — a call your own settings allow will run. Each node's
--tools set and its deny list still bind. Enterprise and managed policy are unaffected by
this flag and cannot be widened by it. Agent mapping and skill activation are off for this
run: a staged definition would be shadowed by a same-named one your settings discover.
Codex, replacing the isolated line at
cmd/oh-my-graph/main.go:1180 for the run that opted in:
Planned nodes run with YOUR configuration (--accept-loaded-user-config): ~/.codex/config.toml,
your repository's rules and AGENTS.md files, and your MCP servers all load. The filesystem
sandbox above and approval_policy="never" are argv on every node, so this flag widens neither.
Both are fmt.Fprint of that literal, two-space indented like every other line
on the screen, and both end with a newline. The Codex line stays inside
noteCodexRuntimePolicy's one-line-per-difference budget by replacing a line
rather than adding one; the Claude paragraph replaces noteCeiling's and is one
line longer than it, which is the price of the grants sentence and is paid
knowingly.
No new snapshot field. runstate.NodeToolPolicy.SettingSources is already a
*string for exactly this reason
(internal/runstate/runstate.go:129-134): "A pointer (not a bare string) so
'omitted' and 'explicitly empty' stay distinguishable across a round-trip." And
a hand-written run records no tool policies at all
(internal/runstate/runstate.go:119-121), so inside a snapshot whose
ToolPolicies map is non-empty, an absent setting_sources is unambiguous: an
opted-in auto run. The answer is derivable; a second field could only ever
disagree with the argv.
It follows that the disclosure predicate reads the policies about to be
spawned, not the flag. One helper over map[string]runner.ToolPolicy,
answering "does any planned node carry the operator's config", serves both
auto and resume, and cannot drift from what is executed.
resume therefore inherits the choice with no new flag —
toRunnerToolPolicies copies SettingSources verbatim
(cmd/oh-my-graph/resume.go:742) and the recorder writes it back
(cmd/oh-my-graph/resume.go:543) — and must print the same line, in the
slot where the leg's other de-escalations already print
(cmd/oh-my-graph/resume.go:528-530), before the banner.
resume registers no opt-in flag, for ADR 0017 §6's reason one direction
over: a resumed leg's own flags may only de-escalate, so there must be no flag
by which a second process widens a ceiling the first leg chose. Inheriting the
first leg's choice is not resume choosing.
chat does not get the opt-in. chat builds its coordinator at
cmd/oh-my-graph/chat.go:120 and registers no such option, so every chat-planned
node stays isolated. ADR 0030 §2.6's reasoning applies with more force here: one
[y/N] keystroke already covers two questions, and a flag that widens an
unattended run must be typed at a launch, not implied by a y.
internal/childenvis untouched. One list, no runtime branch, deletingANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,OPENAI_API_KEYandCODEX_API_KEYfrom every child (internal/childenv/childenv.go:31-38). This feature is about argv and setting sources; it is not about environment, and a restored config is not a restored API key. A diff underinternal/childenv/means the implementation took a wrong turn and should be stopped rather than reviewed.- No fifth exec seam. Exactly four objects spawn a process —
runner.CLIRunner,verify.ShellVerifier,worktree.GitManager,browser.ExecOpener(internal/childenv/childenv.go:5-13), enforced byinternal/invariants/exec_seam_test.go. This ADR adds no spawner and moves no spawn. - Enterprise/managed policy stays unbypassable. §1.1. The opt-in's mechanism
is the absence of
--setting-sources; enterprise settings are unioned on top and were never subtractable by that flag in either direction. The disclosure says so, and §3(11) records why that sentence is a documentation claim rather than a test. runis unchanged in every respect — §1.3.Options.ToolPoliciesstays nil there,policyForkeeps its early return, andTestScheduler_HandWrittenGraphGetsNoCeilingmust pass untouched.- The graph schema is unchanged. No new node field, no new top-level key,
nothing new a planner may write. The opt-in exists only as an operator's argv
and, derivatively, as a shape in
state.json. - The planner and the assessor keep their stances. The planner already runs
unisolated (§1.4) and this flag does not reach it. The assessor keeps layer 1
unconditionally (
internal/coordinator/assess.go:199-206): its input is untrusted model output by design, so its isolation answers a different question and is not the operator's to trade. --dry-runandlintare unchanged. They callnoteCodexRuntimePolicy(..., false)about hand-written graphs (cmd/oh-my-graph/dryrun.go:58,cmd/oh-my-graph/lint.go:94) and already tell the truth.
- Default is OFF, both runtimes. An
autorun with no flag buildsSettingSources = &""andStrictMCPConfig = truefor every planned node — a regression pin on today's behaviour, asserted on the policy map, not the argv. - The opt-in's policy shape. With the option set, every planned node has
SettingSources == nilandStrictMCPConfig == false, whileAllowedTools,ToolsandDisallowedToolsare byte-identical to the isolated run's for the same graph. The table in §2.3 is the assertion. - Codex argv.
buildArgswithSettingSources == nilemits none of--ignore-user-config,--ignore-rules,project_doc_max_bytes=0,mcp_servers={}— and still emits--sandbox <mode>andapproval_policy="never". The second half is the point: it pins §2.6's Codex sentence. - Claude argv.
buildArgswithSettingSources == nilandStrictMCPConfig == falseemits neither--setting-sourcesnor--strict-mcp-config, and still emits--allowedTools,--toolsand--disallowedTools. This is the test that makes §2.3's "exactly a hand-written node's config posture, exactly a planned node's tool posture" checkable. WithLoadedUserConfigforces the de-escalations. A plan built with the option has nonode.Agent, noPluginDirs, noSkillin any node'sTools, and carries the printed reason — asserted through the Option alone, with no CLI flags, so the guarantee is the coordinator's and not the call site's (§2.4).- The disclosure slot says one or the other, never both, never neither. Four cases: {Claude, Codex} × {off, on}. Off prints today's strings verbatim (§1.5); on prints §2.6's strings verbatim; the other never appears. Assert the whole literal, not a substring — a disclosure test that matches a prefix is how a sentence loses its second half.
--plan-onlyprints it. Same screen, same strings, no run minted.- The goal loop's later cycles print it. Cycle 2 of a
--max-cyclesrun re-entersprintPlanForRuntimeviacmd/oh-my-graph/goal.go:101; the line must appear on every cycle, since every cycle plans afresh. resumeinherits and discloses. A snapshot with a non-emptyToolPoliciesmap whose entries omitsetting_sourcesresumes withSettingSources == nilon the spawned argv AND prints §2.6's line before the banner; a snapshot withsetting_sources: ""does neither.- The resumed leg does not ERASE the choice. After a resumed leg settles a
node, the rewritten
state.jsonstill omitssetting_sourcesfor every planned node. This is the exact defect ADR 0030 hit onbuild_evidence(0030's header, "the resumed leg's recorder erased it"), on a recorder that writes the whole snapshot on everyRecordNode. Write this test first. - The child environment is untouched by the flag. A spawn under an
opted-in policy still has all four variables of §2.8 scrubbed. This is the
guard that turns "we did not mean to touch
childenv" into an assertion. (Its neighbour claim — that enterprise/managed policy survives — gets no test: no CI machine here has a managed settings file, and a test that cannot fail is worse than a stated claim. §8 does not require it either; it is inherited from the flag's absence, not from new code.) chatcannot reach it. A chat-planned graph is isolated, and no chat surface parses the flag.runis untouched.TestScheduler_HandWrittenGraphGetsNoCeilingpasses unmodified.
- NO FLAG; document
--plan-only+run. Argued in §2.1 and rejected because the workaround it blesses is wider than the flag it avoids, and because it puts the goal loop permanently out of reach for anyone whose nodes need their own MCP servers. It remains documented and supported; it is simply not the only door. - Codex-only. Rejected in §2.2. It would have been the smaller diff and would have said, without arguing it, that the Claude ceiling is worth more than the Codex one.
- Per-node opt-in in the graph schema. Rejected in §2.1: the planner writes
the graph, and a planner able to request the operator's settings is a sharper
form of the hole
validatePlannedNodeAgentexists to close. - Layer 1 off, layer 4 left on. Rejected in §2.3: the disclosure would then
claim MCP servers load while
--strict-mcp-configsat on the argv, over an unmeasured E5. A disclosure whose truth depends on an unmeasured flag is not a disclosure. - Keep agent mapping and skill activation on under the opt-in. Rejected in
§2.4 against measurement (j): arm
Xshows the repository's same-named copy winning 3 of 3 undernil. Shipping both would leave ADR 0017's and ADR 0022's guarantees written down and false. - A new
state.jsonfield recording the choice. Rejected in §2.7: the policy map already answers the question unambiguously, and a second source of truth about the argv can only ever disagree with the argv. - Let
resume --accept-loaded-user-configwiden a leg. Rejected in §2.7: a resumed leg's flags de-escalate only.
- The operator reads it as pure capability. They wanted their CLAUDE.md and
did not notice that on Claude their standing
Bash(*)came back with it, so a planned node does something its declared scope appeared to forbid. This is the most likely failure and the whole reason §2.6's Claude line spends a sentence on grants and the name spends a word onaccept. It is mitigated, not closed. - The flag becomes the default in practice. An agent that keeps hitting a denied tool reaches for the widest flag it can find; a human aliases it. §7 makes this the falsifier rather than pretending a name prevents it.
- A resumed leg widens silently. Covered by §2.7 and tests 9 and 10; without the resume-side disclosure, a second process would spawn config-carrying nodes with nothing on screen saying so. This is the failure mode most likely to be missed, because the first leg's screen is long gone.
- A repository shadows a staged skill or agent. Prevented rather than
disclosed, by §2.4's forced de-escalation. If a later change reopens the
combination, measurement (j) arm
Xis the pre-written proof that it is wrong. - The
--strict-mcp-configdrop is read as a security regression. It is a widening, and it is chosen; it is bounded to opted-in planned nodes and stated on screen. Every other node in the project keeps layer 4.
- Behaviour with no flag: identical, everywhere. Same argv, same screens,
same
state.json. Every existing test of the ceiling must pass unmodified — if one needs editing, the default moved and the change is wrong. state.jsonis forward and backward readable.setting_sourcesis alreadyomitemptyon a*string; an old reader sees an absent field and an old snapshot round-trips unchanged.- The graph schema and
runare untouched, so no saved plan, fragment or template needs a change and nographs/*.yamlin this repository is affected. - Documentation surfaces owed by the implementation lane (not by this node):
README.md:194-203 — the "One boundary to read before you trust it" paragraph
whose sentence "Codex discards user config, project rules/AGENTS files and MCP
servers for planned nodes" (README.md:198-199) becomes conditional and must
say so on both runtimes; SECURITY.md's layer table (SECURITY.md:220) and its
layer-4 note (SECURITY.md:327); DESIGN.md's ceiling section (DESIGN.md:2380-2391);
docs/LIMITATIONS.md; and a## [Unreleased]CHANGELOG entry. No release, no tag, no version bump belongs to this decision.
This decision is wrong if any of these is observed.
- The residual layers do not survive restored settings. If a measurement
shows that with settings loaded a node's
--toolsnarrowing or its--disallowedToolsalso stops binding, then the opted-in node is not narrower than §2.1's--plan-only+runworkaround, and the flag's entire justification collapses — it would then be a second spelling of a door that already exists, and should be deleted in favour of documenting the first one. This is §8's required measurement. - It becomes the normal way to run
auto. Concretely: the flag appears in anygraphs/*.yamllane's documented invocation, in the README quickstart, or in this repository's own dogfooding commands. That would mean the ceiling is off by default in practice, and the honest response would be to renegotiate the ceiling in the open rather than keep a default nobody uses. - An issue reports an opted-in node doing something the operator believed
allowed_toolsforbade. The name and §2.6's Claude sentence failed to carry the grants half of the bill; the disclosure, not the mechanism, is the defect. - Nobody uses it, and operators keep being pointed at
--plan-only+run. The flag bought nothing and should be removed rather than maintained. - A request arrives for per-node granularity that §2.1's reasoning cannot answer — e.g. one node legitimately needing an MCP server while the rest of the run must stay isolated. Whole-run granularity would then be the wrong unit, and the untrusted-planner argument would need a mechanism (an operator annotation applied after validation) rather than a refusal.
One arm, on both runtimes, and it is falsifier 1:
A planned node whose
--toolsomitsWriteand whose--disallowedToolsnamesBash, run withSettingSources = nilunder a user settings file grantingBash(*)andWrite, asked to do both. Expected: neither runs — layers 3 and 5 bind independently of settings — while the same node'sBash(git *)scope is observably NOT enforced, which is the bill §2.6 discloses.
Recorded under docs/measurements/ with argv committed, pre-registered
outcomes, and 3 spawns per arm, matching the shape of measurement (j). Until it
exists, §2.3's central claim is a projection and this ADR stays Proposed.
- Whether Codex's restored
~/.codex/config.tomlcan weaken the sandbox floor.--sandboxand--config approval_policy="never"are on the argv and argv beats config file, which is why §2.6's Codex line says what it says; but no measurement in this repository has fed a hostileconfig.tomlto that argv. The claim is read fromcodex_protocol.go:29-35, not observed. - Whether
--strict-mcp-configcloses MCP at all — E5, unmeasured (DESIGN.md:2919-2923). §2.3 routes around the question rather than answering it; dropping the flag makes the disclosure true either way. - What Claude's default source set is when
--setting-sourcesis omitted. This ADR asserts only what this repository can warrant: the flag is absent, which is exactly what the planner call (§1.4) and every hand-writtenrunnode (§1.3) already get, and whatTestScheduler_HandWrittenGraphGetsNoCeilingdescribes as "the user's own settings, hooks, MCP servers and tool permissions". The precise set is the CLI's to define and may change under it.