nextv1.4.0
Reference

Permission gate

What Kraft's permission gate does per harness, how it decides, and the keys that configure it.

Kraft's permission gate answers a worker's tool-permission request from the task's own policy. Each answer, allow or deny and why, lands on the work item's timeline. For the reasoning behind this design, see Why a permission gate.

Which harnesses reach the gate

HarnessMechanismReaches the gateTimeline eventsGrants
ClaudePermission prompt toolYesYesYes
CursorpreToolUse hookYesYesYes
CodexPreToolUse hook, trusted per launchYesYesYes
OpenCodeRules written at launchNoNoNo
AmpRules written at launchNoNoNo
GeminiIts most permissive unattended mode (--approval-mode yolo)NoNoNo
AntigravityIts most permissive unattended mode (--dangerously-skip-permissions); a task with deny_tools or allowed_tools is refusedNoNoNo

Under a sandbox with network:, the Codex hook reaches Kraft through the sandbox's own route out: its command is Kraft's kraft shim, mounted read-only in every such container at /opt/kraft/bin, which asks the permission gate in the Kraft server as that session and denies whenever it gets no answer. The hook is trusted by the hash the image's own codex gives, asked in a short container with no network: Codex skips an untrusted hook without a word and runs the call, so a task whose image cannot vouch for the hook is refused. The launch also switches hooks on and marks Kraft's enabled, so a Codex config a worker writes into its home directory cannot turn the hook off. A sandbox without network: has no route to Kraft, so there a Codex or Cursor task with something to enforce is refused instead of run unenforced, and the refusal says so. (Cursor ignores a proxy, so a sandbox with network: refuses it anyway.) Claude asks the same way: under network: it is launched with Kraft's own MCP server at http://kraft/mcp and no other, which answers its permission tool for that session alone. Without network: a sandboxed Claude task is refused whatever its policy, since its permission tool would be missing and the CLI exits at start. See Callbacks from a sandbox. OpenCode and Amp keep their mechanism.

OpenCode and Amp still enforce deny_tools and allowed_tools through rules written at launch. See How each harness runs unattended.

Claude

Kraft launches Claude with its own MCP tool, mcp__kraft__permission_request, as the permission prompt tool. Claude calls it for any tool call its own classifier does not settle, and Kraft answers from the task's policy.

What reaches the gate depends on permission_mode:

Task's allowed_toolsClaude's modeWhat reaches the gate
Not setautoOnly calls Claude's classifier declines to settle.
SetmanualEvery call the built-in tool-name allowlist does not cover.

Cursor

Cursor runs a preToolUse hook before every tool call, ahead of its own --auto-review classifier. When a Cursor task's policy has something to enforce, Kraft writes an entry into the worktree's .cursor/hooks.json that runs kraft admin permission-hook cursor. Something to enforce means an allowed_tools list, any deny_tools, or a grant other than git-commit. With nothing to enforce, Kraft writes no hook.

  • Your repository's own hooks in that file stay. Kraft's entry sits beside them.
  • The entry stays for the worktree's life and is the same for every launch.
  • If a launch needs the hook and .cursor/hooks.json cannot be read, Kraft refuses the launch and names the file.
  • The file never reaches a commit. Kraft excludes an untracked .cursor/hooks.json through one marked line in the repository's info/exclude, and a tracked one through skip-worktree in that worktree's index. A skip-worktree file can make a rebase that touches it refuse to run.

The hook sees Cursor's tools under the names your policy uses:

Cursor's toolChecked asNote
ShellBash
ReadRead
WriteWrite and EditCursor creates and edits files with it. Denied if either is denied. Allowed under an allowlist only if both are listed.
DeleteDelete
GrepGrepAlso covers Cursor's glob.
Web fetch, web searchnoneNever reach the hook.

Kraft cannot deny a web fetch or web search on Cursor. A Cursor launch whose policy would have to deny one is refused, rather than run with that part of its policy unenforced. That means WebFetch or WebSearch in deny_tools, or an allowed_tools list that does not name both.

The hook asks the gate in enforce mode. Under an allowed_tools list, the launch sets KRAFT_PERMISSION_FAIL_CLOSED=1 in the worker's environment, and the hook fails closed: if Kraft is unreachable, the payload is unreadable or the policy cannot be resolved, the call is denied. Without an allowlist, any of those is no opinion, and Cursor's classifier decides as if there were no hook. Kraft's entry also sets failClosed: true, so a hook that crashes or takes longer than its 10-second timeout denies the call, with or without an allowlist: Cursor would otherwise allow it.

Codex

Codex runs a PreToolUse hook before each tool call. When a Codex task's policy has something to enforce (the same rule as Cursor), the launch adds two -c flags, on the resume command line too:

  • hooks.PreToolUse runs kraft admin permission-hook codex.
  • hooks.state trusts exactly that hook.

Kraft never passes --dangerously-bypass-hook-trust, since that also trusts every hook a repository ships. Kraft writes nothing to your ~/.codex or to the worktree. If Codex cannot list the hook, does not trust it, or does not answer within 15 seconds, Kraft refuses the launch.

  • The hook also fires for Codex's own background agents. A call whose working directory is inside Codex's home ($CODEX_HOME, else ~/.codex) gets no opinion and never reaches the gate. Every other call is asked, wherever it runs.
  • Codex calls its shell Bash. apply_patch, its one tool for creating and editing files, is checked as both Write and Edit.
  • Web search runs on OpenAI's side and never reaches the hook. A Codex launch whose policy would have to deny WebSearch is refused.
  • Fail-closed under an allowlist works as it does for Cursor.
  • No opinion is {}, which leaves the decision to Codex's own reviewer. A deny blocks the call, and the agent sees Command blocked by PreToolUse hook: Kraft: <reason>.

OpenCode and Amp: rules written at launch

OpenCode and Amp get no per-call hook. Kraft writes the task's deny_tools and allowed_tools into the CLI's own permission configuration for that one launch, and the CLI enforces them itself. With neither set, Kraft writes nothing.

OpenCode gets the rules as a permission block in OPENCODE_CONFIG_CONTENT, and runs as opencode run --standalone, because only a standalone run reads that block.

  • A denied tool is deny.
  • An allowlist is "*": "deny" followed by allow for each listed tool.
  • Denying Bash also denies execute, OpenCode's code mode.

Amp gets a settings file of the launch's own, $KRAFT_HOME/run/harness-config/amp/<session>.json, passed with --settings-file. For that run it replaces ~/.config/amp/settings.json, which Kraft never reads or writes. Your Amp login still works.

  • Its amp.permissions rules come before Amp's built-in ones, and the first match wins.
  • A denied tool gets a reject rule and everything else is left to Amp's built-ins.
  • Under an allowlist, each listed tool gets an allow rule and a final * rule rejects the rest. An allowlisted tool is therefore allowed outright: Amp's own built-in asks (a git push, an rm -rf) no longer apply to it.
  • Denying Bash rejects shell_command and its async and legacy forms.
  • Amp has no read, grep or glob tool of its own. Read or Grep in deny_tools refuses an Amp launch, and in an allowlist grants nothing.

On both:

  • A denied tool name that no tool on that CLI maps to refuses the launch and names the tool.
  • An allowlisted name the CLI has no tool for grants nothing. Every tool not listed is denied anyway.
  • A CLI tool that covers two policy names is denied if either is denied, and allowed under an allowlist only if both are listed. This applies to OpenCode's edit and Amp's apply_patch, which each cover Edit and Write.
  • Nothing reaches the gate, so none of this appears as a permission_decision on the timeline.
  • Grants are not applied. A CLI rule like git push * would also match git push x; rm -rf y, so Kraft writes no grant.

How the gate decides

The gate answers from the resolved policy of the task the calling session is running: the same allowed_tools, deny_tools and grants its launch resolved. It checks these rows in order:

The callPrompt mode (Claude)Enforce mode (Cursor and Codex hooks)
Names a tool in deny_toolsDenyDeny
Is an instance of one of the task's grantsAllowAllow
No layer set allowed_toolsAllowNo opinion: the CLI's own classifier or reviewer decides
allowed_tools is set and names the toolAllowAllow
allowed_tools is set and does not name it ([] names nothing)DenyDeny
The policy cannot be resolved (the task or its profile is gone)DenyDeny if the session is fail-closed, else no opinion

A deny is a deny on every harness. An allow is not always final. On Cursor, a hook allow does not override --auto-review, so Cursor's classifier can still refuse a call the gate allowed. Whether a Codex hook allow outranks Codex's reviewer is not guaranteed.

Grants

A grant is a named operation the gate allows a task even outside its allowed_tools: git-commit, git-rebase or git-push. It matches a Bash call only when the command is exactly one plain git invocation of that subcommand. Otherwise the call falls through to the rest of the decision table. A command is refused as a grant if it has:

  • a shell operator, substitution, redirection, subshell, brace or glob expansion, backslash, ! or a newline. A commit message with $ or ! in it, a multi-line one, or one written through a heredoc is not granted. With no allowlist that leaves it to the CLI's classifier. Under an allowlist without Bash it is denied.
  • an env prefix, or anything but git as the first word.
  • a git option before the subcommand other than --no-pager, -P, or -c setting user.name or user.email. -C is refused because it would point git at another repository's config and hooks.
  • an option that runs a command of the caller's choosing: --exec and --receive-pack on push, --exec/-x and --strategy/-s on rebase, and any abbreviation of those long options.

A git-push grant is further held to one named remote and the work item's own branch.

  • Its first positional must be a plain remote name (letters, digits, _, ., -, not starting with ., _ or -), never a URL or path.
  • At least one refspec must follow, and every one must update the item's branch and nothing else: <branch>, refs/heads/<branch>, or <src>: either of those, such as HEAD:<branch>. A push that names no refspec, such as a bare git push, is not a grant: where it goes depends on git config the gate cannot see.
  • Kraft refuses --delete/-d, --mirror, --all (and --branches), --prune, --tags, --follow-tags, --recurse-submodules, --repo, -o/--push-option, and a refspec with an empty <src>, which deletes.
  • Kraft refuses a plain --force/-f and a forced +refspec. --force-with-lease stays allowed, because an escalation that rebased the branch has to force-push it. An escalation turn is told the command: git push --force-with-lease origin HEAD:<branch>.

A grant does not stop git's own hooks. A commit or push under a grant still runs the repository's hooks, including a core.hooksPath inside the tree (.husky/, say), which the agent can edit.

Where a grant takes effect

A grant is the gate's decision, a logged allow on the item's timeline. Whether the CLI then runs the call is the CLI's own business.

HarnessLimit
CursorA hook allow does not override --auto-review, so the classifier can still refuse a granted call.
Claude, no allowlistThe gate already allows every tool, so a grant adds nothing.
Claude, allowlist without BashClaude launches without a Bash tool at all, so a granted git push never reaches the gate.
OpenCode, AmpGrants are not applied.

How grants combine

Grants accumulate down the layers (repository policy:, chain, node, step, task), as deny_tools does. A workspace grants only what every repository in it grants. A work item's own override, and a retry's, can drop a grant but never add one.

An escalation turn holds its node's grants plus defaults.escalation_grants from policy.yaml. Unset, that is all three grants. Set it to a shorter list, or [], to grant escalations less. A gate's reviewer gets no such default. A chain task's git-commit already comes with its launch.

Configuring the gate

The gate enforces whatever allowed_tools, deny_tools and grants resolve to at the task's scope. Set them like any other policy field. See the allowed_tools, deny_tools and grants rows in Configuration for how the layers combine.

Reading decisions

Kraft appends every decision to the work item's timeline as a permission_decision event. The event carries:

  • the tool name, and the session and node it came from;
  • the decision and the reason;
  • the grant that allowed it, if one did;
  • for a hook, the harness and the CLI's own tool name (harness: cursor, cli_tool: Shell).

No opinion is not a decision, so an enforce-mode call left to Cursor's classifier logs nothing. Read the events with:

kraft view events ID --type permission_decision

Add -f to watch a live item.

Copyright © 2026