Saphan StudioDocs
Configuration

Which backends receive a grant

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.

BackendReceives a grant?Where / why
claude-codeYes<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.
codexNoThe 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-compatNoNo 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-loopNosaphan-agent loop takes its whole mandate on argv plus the saphan config ladder, and its own jail deny-reads that ladder.
procNoA 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-codeNo, and this row is the one to read before trusting the othersqwen 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 elseRefusedA 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.

The runbook: setting a grant class and confirming it reached the leg

  1. 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.
  2. 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.
  3. Ask which backends it will reach, before dispatching anything:
    saphan fleet next --rules
  4. 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.
  5. Confirm on disk, for a claude-code leg only. After the worktree exists:
    cat <worktree>/.claude/settings.json
    git -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.
  6. 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.

What to do when it refuses

  • 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 eitherconfinable 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.

If you want a file on a backend that receives none

You can always place one by hand:

saphan dispatch command … --provision /abs/path/to/class.settings.json=<dest>

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.

On this page