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. A key whose effect differs per backend needs its answer written down per
backend, and the engine holds that answer as one row per backend — never one path applied to all.
Backend
Receives a grant?
Where / why
claude-code
Yes
<worktree>/.claude/settings.json. A spawn starts in its working directory and the engine passes the agent no other settings location, so the agent finds a grant there and nowhere else.
codex
No
The engine governs codex's permission surface on its command line, under codex's strict-configuration mode, where a command-line key outranks every layer of codex's own configuration. There is no file left for the engine to compose.
qwen-code
No
qwen reads its own settings vocabulary, not a Claude Code settings document, and the only grant document the engine composes is the latter; copying it one directory over would not be a grant qwen honours. Give a qwen leg its tools on its command line (--allowed-tools) — which the engine does not compose either.
droid
No
The engine composes no grant document for it.
cursor
No
The engine composes no grant document for it — and runs on it are refused by the confinement layer; see the sandbox matrix below.
openai-compat
No
No vendor process runs on the seat at all — this backend is an HTTP request made by the engine, so there is no local program that could read a grant file.
agent-loop
No
saphan-agent takes its whole mandate on its command line and the saphan configuration, and confines itself.
proc
No
A substrate binary is an arbitrary program you name, and the engine composes no vendor configuration for it.
anything else
Refused
A backend nobody has answered for does not get a silent default. Dispatch and worktree creation refuse by name.
⚠ A "No" row is not a statement about confinement. What bounds a leg that receives no grant is
the confinement around it, which differs by backend and platform — read
Sandboxing: which agent is confined, where before assuming either
way.
workspace.grant_class.<role> names a class for one fleet role — master, executor, review,
design, scribe or advisor — and overrides workspace.worktree_grant_class for spawns
carrying that --role. A role with no entry falls back to the workspace-wide class, and the
fallback is recorded on the run.
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. See Permission classes.
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 the dispatch says so.
Dispatch, and read the composition's own answer. Whenever a declared grant is not
provisioned for the leg's backend, the dispatch says so and quotes that backend's reason.
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 every other backend there is nothing to confirm on disk, and that is the correct
outcome. Confirm the reason instead: it is in the dispatch's output and on the run record.
grant-destination-undecided — the backend is known and no answer is declared for it. This
is a gap in the engine, not your configuration, and no flag works around it. Report it.
grant-destination-unknown-backend — the name is not a backend. Check the spelling;
editing the grant class will not help.
A leg reports permission denials and you expected a grant — check the backend first. On
the "No" rows above no grant was ever composed, so a missing grant file is not what denied the
leg; look at the confinement for that backend and platform.
⚠ 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 nobody has shown that a file
placed this way behaves as a boundary for that vendor. Some vendors merge settings from
several scopes or rewrite the settings file they are handed, which makes provision-then-verify
unreliable.