Configuration and launch precedence
A displayed setting answers a configuration question. It does not necessarily answer the wrapper’s final policy question.
Nia’s fictional settings view says always-ask, yet an exec-tier recording call proceeds without a question. Before assuming a broken gate, she checks whether the launch supplied autoApprove separately.
Keep persisted configuration and runtime overrides separate
The supplied Settings implementation merges the active profile’s persisted layer, project settings, explicit configuration overlays, and runtime overrides, in that increasing order of precedence. get() resolves a value from the merged view, falling back to the schema default when it is absent.
Settings.set() changes the persisted layer and queues saving. Settings.override() changes a non-persisted runtime layer. An ordinary session transition is not a factory reset of those settings.
For well-formed object layers, maps are deep-merged; arrays such as bash.patterns are replaced by the higher layer rather than concatenated. Therefore the effective ordered rule list must be inspected as a list, not imagined as every rule from every file in sequence.
The wrapper reads mode and user policies from its execute-time context.settings. A tool’s own classifier can also read settings bound to its tool instance—for example, BashTool.approval() reads its session’s bash.patterns. A dump from an unrelated settings instance cannot establish both inputs.
Read the isolated CLI observation
The supplied configuration report used the installed omp/18.0.7 CLI, an exclusively owned fresh named profile with no linked authentication, and an empty temporary working directory. It did not inspect or change the reader’s personal settings.
This section reproduces the observation, not an executable recipe. The report does not include a complete profile-creation preamble, so no launch or profile-creation command is invented here. The default route is to read the recorded roundtrip.
Setting tools.approvalMode to write with JSON output returned:
{"key":"tools.approvalMode","value":"write"}
Setting the tools.approval record to the JSON object {"bash":"deny"} returned:
{"key":"tools.approval","value":{"bash":"deny"}}
The saved configuration was YAML:
tools:
approvalMode: write
approval:
bash: deny
Getting each setting with JSON output returned an object, not merely its value:
{
"key": "tools.approvalMode",
"value": "write",
"type": "enum",
"description": "Default approval behavior for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves all tiers; user policy may still prompt or block."
}
{
"key": "tools.approval",
"value": { "bash": "deny" },
"type": "record",
"description": "Per-tool approval policies. Set to 'allow' to auto-approve, 'prompt' to require confirmation, or 'deny' to block. Overrides are honored in every approval mode."
}
Those descriptions are the observed CLI copy. Their shorthand about read-only/workspace-write behavior does not establish containment or override the more precise resolver precedence. The supplied capture also contains wall-time annotations; those are not fields in either JSON setting object.
Recorded boundary: the roundtrip demonstrates configuration serialization and inspection. It does not demonstrate an approval dialog, a live session override, effective wrapper policy, or a model invocation. commands/config.ts delegates to runConfigCommand(); the full delegated CLI implementation is not in this selected pack, so the observed JSON shapes are kept attached to the report.
Launch flags add a second source of precedence
The supported built-in long forms are --approval-mode, --auto-approve, and --yolo. The latter two set autoApprove: true. There is no listed built-in -y alias in the supplied parser.
These are flag fragments to interpret, not commands to launch for this exercise:
| Launch input | Settings value after root-command override | Wrapper mode when the corresponding context is supplied |
|---|---|---|
| No approval flag | Existing resolved setting | That setting, with the wrapper’s documented fallback if absent |
--approval-mode always-ask | always-ask, runtime only | always-ask |
--auto-approve or --yolo, without an explicit mode | yolo, runtime only | yolo |
--approval-mode always-ask together with --auto-approve or --yolo | always-ask remains the displayed runtime setting | yolo, because context.autoApprove === true wins in the wrapper |
main.ts deliberately preserves the explicit mode in settings when both inputs exist. buildSessionOptions() separately forwards autoApprove, and the SDK supplies it in the tool context. The wrapper chooses yolo from that boolean before calling resolveApproval().
Changing the order of those two different flags is not a way to make the explicit mode outrank the boolean at the wrapper. They populate separate inputs.
Recorded comparison: boundary-auto-approve-mode injected configured always-ask, autoApprove: true, and no UI. It returned with zero prompts and one inert execution. boundary-auto-approve-still-denied added an effective user deny and stopped before handlers, prompts, or execution. These cases verify execute-time wrapper behavior, not CLI parsing or a full launch.
Invalid input does not create a new safe mode
The --approval-mode setter accepts exactly the three documented values. For an invalid supplied value it logs a warning and does not install that value as the parsed mode. That is not the same as a hard launch refusal, nor proof that it selected always-ask. Other parsed inputs and the resolved settings remain relevant.
Similarly, a type annotation on Settings.get() is not runtime validation of every hand-edited value. Keep the resolver’s normalization of individual policy strings separate from raw record shape, launch parsing, and other consumers.
Paper checkpoint: a settings view says always-ask, but the execute-time context has autoApprove: true. What mode does this wrapper use, and can an effective deny remain? Worked answer: yolo; yes, the resolver’s explicit deny branches still apply.
Failure boundary: no configuration change is required to finish this chapter. Do not use a personal profile to recreate the observation or treat a config getter as a diagnostic endpoint for another process’s effective policy.
Source anchors: packages/coding-agent/src/config/settings.ts — Settings.get, set, override, #rebuildMerged; packages/coding-agent/src/config/settings-schema.ts — tools.approval, tools.approvalMode, bash.patterns; packages/coding-agent/src/commands/config.ts — Config.run; packages/coding-agent/src/cli/args.ts — parseArgs; packages/coding-agent/src/cli/flag-tables.ts — STRING_SETTERS["--approval-mode"], VALUELESS_FLAGS; packages/coding-agent/src/main.ts — runRootCommand, buildSessionOptions; packages/coding-agent/src/sdk.ts — execute-time context construction. CLI observations: proof/cli-config-proof.json.
Tool permissions and approvals · Source chapter: permissions/configuration-and-launch-precedence. Original evidence remains scoped to its recorded snapshot.