A permission grant class is your act; where it lands is a per-backend answer.
Setting workspace.worktree_grant_class is your act, and its effect depends on the
backend the leg runs. This chapter exists because that was not true before — one hardcoded
path was applied to every backend, so a codex seat got a file it never opens — and a key
whose effect differs per backend needs its answer written down per backend.
The answer is one row per backend, held in the engine's seat dictionary and readable
live. Ask the machine rather than this table:
saphan fleet next --rules
It prints one grant dest line per backend. The table below is what those rows say today.
Backend
Receives a grant?
Where / why
claude-code
Yes
<worktree>/.claude/settings.json. Workdir-relative and one level under the worktree root because a spawn starts with cwd = workdir and the engine composes neither --settings nor --add-dir — the vendor finds a grant there and nowhere else.
codex
No
The engine governs codex's permission surface on the argv: sandbox_workspace_write.exclude_slash_tmp, exclude_tmpdir_env_var and network_access=false are pinned with -c under --strict-config, where a CLI key outranks every layer of codex's own config ladder. There is no file left for the engine to compose.
openai-compat
No
No vendor process runs on the seat at all — this backend is an HTTP door, so there is no local program that could read a grant file.
agent-loop
No
saphan-agent loop takes its whole mandate on argv plus the saphan config ladder, and its own jail deny-reads that ladder.
proc
No
A substrate binary is an arbitrary program you name, and this engine composes no vendor configuration for it. ⚠ It composes no jail for it either — see the bounds table in §5; this row used to say "what bounds it is the OS-level jail", which round 6 measured false.
qwen-code
No, and this row is the one to read before trusting the others
qwen does read <workdir>/.qwen/settings.json, and that scope beats its profile home — so unlike the rows above, the "No" here is not about the vendor. It is about the engine's artifact: the only grant document this engine has is a Claude Code settings file, and qwen's vocabulary is its own (tools.allowed / permissions.allow over write_file, edit, run_shell_command). Copying the claude document one directory over would be the vendor-shaped default this chapter exists to delete. Three measured properties would defeat a naive copy anyway (qwen 0.22.2, see docs/Qwen Code §4): the vendor rewrites every settings file it reads, project settings apply only to a trusted workspace, and --safe-mode discards every settings-sourced tool rule. Give a qwen leg hands on the argv (--allowed-tools), which the engine does not compose either — that gap is recorded, not hidden.
anything else
Refused
A backend nobody has answered for does not get a silent default. Dispatch and worktree creation refuse by name — "nobody decided" is not evidence that no grant is needed.
Write the class file. It is an ordinary Claude Code settings file. Only its
permissions and env blocks are read; both are copied byte for byte, never
re-authored.
Declare it.
saphan config set workspace.worktree_grant_class /abs/path/to/worktree-toolchain-grant.settings.json
A relative path resolves against the workspace root. If the deprecated run.grant is
also set to a different path, this key wins and composition prints a named warning
saying so.
Ask which backends it will reach, before dispatching anything:
saphan fleet next --rules
Dispatch, and read the composition's own answer.saphan dispatch command prints a
NOTE row naming the backend and quoting that backend's own reason whenever a declared
grant was not composed. A dispatch with no grant and no note is a bug — report it.
Confirm on disk, for a claude-code leg only. After the worktree exists:
cat <worktree>/.claude/settings.jsongit -C <worktree> status --porcelain
The file carries your class's blocks, and git status is silent about it — the engine
adds the destination to the repository's shared .git/info/exclude, so the copy never
shows up as an untracked change in any worktree of that repo.
For the other five backends there is nothing to confirm on disk, and that is the
correct outcome. Confirm the reason instead: it is on the composition's NOTE row
and on the run record.
grant-destination-undecided — the backend is in the dictionary and its row answers
nothing. This is an engine gap, not your configuration: it is fixed by declaring that
row's answer in the seat dictionary, and no flag works around it. Report it.
grant-destination-unknown-backend — the name is not a backend. Check the spelling
against the list saphan fleet next --rules prints; editing the grant class will not
help.
R27 on an ssh dispatch — the remote grant rung refused fail-closed for the same
reason as above. Same two remedies.
A leg reports permission denials and you expected a grant — check the backend first
(steps 3 and 4). On the five No rows above, no grant was ever composed, so a missing
grant file is not what denied the leg. ⛔ And do not read those five as unjailed either
— confinable is false for FOUR of them in runner.backendTraits, so whatever removed
those legs' hands came from somewhere this engine does not write. The fifth, qwen-code,
is the exception and it is host-dependent: confinable is TRUE there and
confinableOnlyOn is linux, so a Linux qwen leg IS jailed by this engine while a macOS one is not. ⚠ Do not guess the answer from this bullet: read
the bounds table below, which names it per backend and is re-checked against that
dictionary on every test run. On qwen-code under macOS it is the vendor's own headless
default: give it --allowed-tools, and a grant class will
not reach it.
⚠ Choosing <dest> there is your claim about that vendor, not the engine's. The engine
declines to guess where another vendor's permission grant lands, and for codex it asserts the
opposite — that the governing surface is the argv. Nobody has yet measured whether a file
under $CODEX_HOME would behave as a boundary; on qwen-code, the one measured sibling,
settings resolution is a per-key deep merge, hooks arrays concatenate, the seat home is the
weakest scope, and the vendor rewrites the file it is handed — which makes
provision-then-verify fail by design.