v2.0.0next
Security and project

Security

What Kraft does on your machine, the few settings that make it safe to run, and the threat model behind them: what it protects against and what it does not.

Kraft runs coding agents on your machine, in git worktrees of your repositories. Without a sandbox, an agent runs as your user and can do whatever you can. Kraft binds to 127.0.0.1, and on that address its API needs no login, so any local process can drive it.

To report a vulnerability, see Reporting a vulnerability.

Run it safely

These settings matter most. Each one is a line here and a link to the page that explains it.

  1. Run each repository's agents in a sandbox. Without one, an agent has your SSH agent, your logins and your home directory.
  2. Give the sandbox a network: policy. A launch into a sandbox without one is refused, whatever the harness, because that sandbox's network is not restricted and the worker has no channel to Kraft. unrestricted_network: true on the sandbox accepts that knowingly: the launch runs and gates are not enforced. Cursor ignores a proxy, so network: refuses it: it runs in a sandbox only with unrestricted_network: true and without a tool policy. See Network policy.
  3. Keep Kraft on loopback. To reach the board from another device, set an access password first and use Tailscale: Remote access.
  4. Protect your default branch with a required review from someone other than the account Kraft uses. A sandboxed worker under the network: policy of step 2 cannot approve its own gate; an unsandboxed one can. See Agents and gates.
  5. Do not point Kraft at content you do not trust: a repository, a trigger source or a beads queue. A sandbox does not cover Kraft's own push from the host, where the repository's pre-push hook runs as you. See Prompt injection.
  6. Give CI and webhook senders the trigger-token, never the mcp-token. See The bearer token.
  7. Review a harness profile's executable and a repository's setup_command after anyone else has had access to Settings. See Files and settings.

Default posture

This is what a fresh install does before you change any setting.

AreaWhat a fresh install doesTighten it
SandboxNone. No repository sets sandbox:, so every agent runs directly on your machine, as your user.Set sandbox: on a repository.
Files and loginsA worker reads anything your user can: ~/.ssh, your forge CLI's login, your agent's login, and $KRAFT_HOME, including the bearer token. It can push with your SSH agent.Run it in a sandbox.
Worker environmentAn allowlist, but it includes HOME and SSH_AUTH_SOCK and passes ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN through.Environment variables lists it; a sandbox credentials entry, which needs network:, keeps a variable out of the container.
NetworkWithout a sandbox, a worker reaches any host your machine can. A sandbox without a network: policy is refused unless it sets unrestricted_network: true, which leaves egress open.Add network: to the sandbox.
Claude CodeShipped chains run it with --permission-mode auto and no tool list. Its own classifier decides most calls. A call that reaches Kraft's permission gate is allowed, because no allowed_tools is set, except Monitor and your deny_tools.Set allowed_tools and deny_tools: Permission gate.
Gemini and AntigravityNever reach the gate. Gemini runs with --approval-mode yolo and Antigravity with --dangerously-skip-permissions, so every call is approved. Neither can enforce a tool list, so a launch with one is refused.Unattended runs has the flags.
OpenCode and AmpNever reach the gate. OpenCode runs with --auto and Amp asks for no approvals. Both enforce deny_tools and allowed_tools through rules Kraft writes at launch.Unattended runs.
Codex and CursorThey reach the gate through a pre-tool hook, installed only when the task's policy has something to enforce. With no allowed_tools, the gate gives no opinion, so the CLI's own mode decides every call outside deny_tools and the grants.Permission gate.
Codex and Cursor, when Kraft cannot answerThe CLI's own mode decides, unless the task holds allowed_tools: then the hook denies. In a sandbox under network:, the hook denies on any failure.Permission gate.
Escalation turnsThey hold the git-commit, git-rebase and git-push grants, which the permission gate allows. They can push their item's branch, including with --force-with-lease.defaults.escalation_grants: [] removes the gate's allow only. See Escalation turns can force-push.
Local APIWhile Kraft is bound to 127.0.0.1, a request from this machine needs no password or token. Any local process or user can file, start and approve work.Not possible on loopback. See Out of scope.
GatesMost shipped gates wait for you. See Agents and gates.Require a review on your forge.
An agent deciding its own gatesAn unsandboxed worker can. See Agents and gates.Sandbox it with a network: policy, and require a review.

Threat model

Kraft runs on your machine, binds 127.0.0.1 by default, and edits your repositories through git worktrees using whatever coding agent you have configured. Five threats shape it:

ThreatWhat stops itSection
A web page in your browserA loopback bind that checks the Host, no framing, and no images fetched from other sitesNetwork and browser
Another machine on your networkA loopback bind by default; a password, allowed_hosts and a login throttle when you expose itNetwork and browser, Login
An agent acting beyond its own work itemRefused item verbs, a sandbox, and a network policyAgents and gates, Workers and the sandbox
A repository's content or settingsSearch reads only regular files inside the repository; Settings need the password only off loopbackFiles and settings
Text an agent reads that steers itNothing filters it. A sandbox with a network: policy, a tool policy and the gates limit the damagePrompt injection

What Kraft does not try to protect against is under Out of scope.

Prompt injection

Everything a worker reads is input to the agent, and text in it can steer the agent. That includes:

  • the repository's files, including ones other people wrote;
  • a work item's title and description, whether a person typed it, a trigger posted it or auto-intake copied it from a bead;
  • review comments on the merge request, which the feedback loop asks the agent to address.

Kraft does not filter or sanitize any of it. What limits the damage is what limits any other agent mistake: a sandbox with a network: policy, a tool policy (allowed_tools, deny_tools), and the gates, where a person reads the change before it merges. Under the default posture, an injected instruction runs with your user's access.

Do not point Kraft at a repository, a trigger source or a bead queue whose content you do not trust. A sandbox limits what the agent can do, but not what runs as you on the host: Kraft pushes the branch from the host, and the repository's own pre-push hook runs then.

Out of scope

Kraft does not try to protect against these:

Not protectedWhy
Other users and processes on the same machineThe loopback API has no login, so anyone who can open a connection to it can drive Kraft. $KRAFT_HOME/run itself is 0700, so other users cannot read the databases and logs in it.
An agent running without a sandboxIt has your user's access. See Default posture.
The agent CLI and its providerKraft runs the CLI you installed. What that CLI sends to its model provider, and what the provider keeps, is between you and the provider. See Data and privacy.
A repository's own commandsIts setup_command and test commands run with the worker's access, and a pre-push hook runs as you when Kraft pushes the branch.
Anyone who has your password or the bearer tokenEither one grants full control.
The machine and the forgeA compromised OS, container runtime, gh or glab login, or git remote is outside what Kraft can check.

Network and browser

  • Loopback by default. kraft admin start refuses to bind a non-loopback address without a password set in access.yaml. There is no way to expose the API or UI on a LAN or the internet without opting in.
  • Remote access is the recommended way to reach the board from a phone or another machine: bind the machine's Tailscale address. A bare --host 0.0.0.0 also listens on your LAN, and a Cloudflare Quick Tunnel is the public internet, guarded by the password and a login throttle.
  • No framing by another site. Every response carries Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN. Your browser refuses to show the board inside a frame on any other site. Without this, a page could hide the board under its own content and turn your click into Resume or Approve (clickjacking). That matters most on a loopback bind, where the board needs no login.
  • The API docs pages load nothing unpinned. /docs and /redoc need no login. Their scripts come from a CDN, each pinned to one release and checked by hash. Their policy lets an image load only from Kraft itself or inline, so no other site learns your Kraft's address from them. See The schema.
  • A document cannot make your browser fetch another site. Specs, plans, notes, review threads and gate pages are Markdown a worker can write. An image in them shows only when it comes from the board's own address or is inline (data:). Any other image shows as a link, image: host/path, and your browser fetches it only if you click.
    Otherwise a sandboxed worker could put data in an image's URL and have your browser send it out, past the worker's network: policy.

Host allowlist

The rules differ by bind:

  • A loopback bind answers only to 127.0.0.1, localhost and [::1], so a web page cannot reach it under another name. A trailing dot (localhost.) or a longer spelling of ::1 is the same name, and is accepted too.
  • A non-loopback bind checks the Host header against allowed_hosts in access.yaml for every request that carries Sec-Fetch-Site, Origin, Referer or a page Accept. That covers every page load and login: a password alone does not authorize an arbitrary hostname.
  • Each entry is kept in the form the Host is compared in: lowercase, with no scheme, port or trailing dot. A wildcard is refused when you save it, and kraft admin doctor names one already in the file.
  • This machine's own 127.0.0.1, localhost and [::1] always pass, since a browser sends them only when it is on this machine.
  • A request with none of those headers, as the CLI, MCP and curl send, needs no entry. It still needs the password for anything but the unauthenticated routes such as /api/health.
  • The board's live event connection, the /api/ws/events WebSocket, follows the same Host rules, and refuses an Origin from another site. The board's own page and a page on this machine pass.

Agents and gates

Which gates wait for you. The shipped default chain stops and waits for you to approve the spec and the plan, before it opens a draft merge request, and again before it marks it ready. There are two exceptions:

  • A spec or plan you attach at intake arrives already decided, so its gate is dropped.
  • The chain_revision_approval gate passes on its own when the revision proposes no change, which is the usual case.

If your forge requires a review, the item then waits for that approval before it merges. A gate you give an auto_review task lets that agent's verdict approve it.

The ten item verbs are approve, reject, pause, resume, skip, abandon, retry, complete, cancel and escalate. The rules differ for a sandboxed worker and an unsandboxed one.

A sandboxed worker under a network: policy cannot decide its own gates.

  • It reaches Kraft only through its session's channel, which acts as that session. The channel allows only reads, progress, thread replies and permission asks on the worker's own item, and an escalation turn's retry of it.
  • It cannot use any of the ten item verbs on its item, revise its attachments, overrides, policy or budget, or file work, whatever it sends. Anything else is refused and recorded.
  • A permission hook that gets no answer denies. See Callbacks from a sandbox.

A sandbox with unrestricted_network: true has no channel, but nothing blocks a direct connection. Without network: the container has the runtime's default network, which is why Kraft refuses such a launch unless the sandbox sets that key. Kraft gives it no channel and no kraft shim, and it adds nothing that stops the container dialing the host. Whether it can reach Kraft's loopback-bound API depends on the runtime.

If it can, Kraft cannot tell the worker's calls from yours, because the loopback API needs no login. Do not count on such a sandbox to keep a worker from approving its own gate.

An unsandboxed worker can, because it runs as you. It can read Kraft's token, write Kraft's database and edit its config. No check inside Kraft can stop it approving its own gate or starting work it filed.

What refuses a worker acting on its own item. Three checks do, and none is a boundary against a worker that is not sandboxed.

  • The kraft CLI and the MCP server refuse a worker's item verbs on its own item, and its changes to that item's attachments, overrides, policy and budget. They check this on the client, from the item the worker stands in.
  • The API refuses the same calls when the request carries the worker's session id (X-Kraft-Session-Id). A caller that leaves the header off passes.
  • The review tools are refused by the API from any request that carries a session id, on any item, not only the worker's own.

The MCP tools page lists every refusal.

Filing and starting in one step. The MCP create tool has no autostart option, so everything filed through it lands paused. A person starts it, from the board or by asking their agent to resume it. The server refuses kraft item create --autostart (403, nothing filed) from a request that carries a Kraft session id or the MCP client tag; see kraft item create.

The server cannot tell a coding agent that runs kraft item create --autostart in a shell on your machine from you, so that call starts the item. Anyone with a shell can start work this way.

What this does not stop. These refusals stop an agent following the normal path, not one working around it. If a gate must hold against the agent, follow steps 1, 2 and 4 of Run it safely. See Default posture for why a sandbox is the line, and Use Kraft from your agent.

Escalation turns can force-push

An escalation turn is the agent Kraft starts to help with a stopped item. It can commit, rebase and push its item's branch, and a push can be a force-push.

The turn is told to push only that branch, with --force-with-lease. Its git-push grant is the permission gate's allow for a push to a plain remote name whose refspecs all update the item's own branch and nothing else (see Grants). The grant does not require --force-with-lease: a push without a force flag matches too.

A plain --force, -f or a + refspec does not match the grant, but nothing blocks it either. On Claude, the call goes to Claude's classifier, or to the permission gate, which allows it when no allowed_tools is set. On Codex and Cursor, with no allowed_tools, the gate gives no opinion and the CLI's own mode decides. The other harnesses never reach the gate: see Default posture.

In Kraft's gate, only a deny_tools entry, an allowed_tools list that leaves it out, or (on Cursor) a write to Kraft's hook file stops a call the grant does not cover.

A grant is the gate's allow only: on Cursor, a hook allow does not outrank --auto-review, so whether the CLI runs the call is up to Cursor.

Kraft starts a turn by itself when an item gets stuck: a task still failing after recovery and the fix loop, or a fix loop that hits its cap, stalls on findings that stop changing, or whose judge stops it. This is auto_escalate_stuck, on by default, up to three times by default (auto_escalate_stuck_cap). Config errors, budget and time caps, infrastructure failures, wait timeouts and a worker's question are not answered this way.

The turn holds the git-commit, git-rebase and git-push grants:

  • A grant is allowed even under an allowed_tools list. Only deny_tools overrides it.
  • A grant does not stop git's own hooks, which the agent can edit.

The turn runs as a person's session, not a worker's. The CLI does not treat it as a worker acting on its own item. The API refuses it, by its session id, only the calls listed further down. A turn that is not sandboxed can:

  • Complete, cancel or abandon the item.
  • Change its agent overrides, and the overrides of a node that has not started.
  • Ask for a retry of a needs_human item, which Kraft performs once the turn ends.

A sandboxed turn under a network: policy has only its session's channel, as a sandboxed worker does. Of the calls above, the channel carries the retry and nothing else.

The turn's prompt tells it not to skip or abandon the item, and to give you the command instead. That is an instruction, not a check: no check stops an abandon from a turn that is not sandboxed. Kraft's own prompt does not mention complete or cancel.

While a turn runs, the API answers 409 to these calls on its item, whoever makes them:

  • Skip, resume and a second escalation, the turn's own calls included.
  • Pause, because the item is stopped and pause takes only a running one.
  • A change to its attachments, because the item has started.

Those depend on the item's state, not on who is calling.

The API also refuses the turn a few calls by its session: approve and reject, raising the budget, changing the budget or policy, and a reviewer's calls. A gate, a review and a spending cap are a person's decision.

A reviewer's calls are opening, editing, deleting, resolving or reopening a thread, adding, editing or deleting a comment, and submitting a review. A turn can still reply to a thread.

Those refusals apply when the call carries a session id. The check reads the X-Kraft-Session-Id header and is a consistency check, not a security boundary. A turn that is not sandboxed can leave the header off.

Set defaults.escalation_grants: [] in policy.yaml to remove the grants. That removes the gate's allow only. On Claude, the turn's pushes then go to Claude's classifier, or to the permission gate, which allows them when no allowed_tools is set. On Codex and Cursor, with no allowed_tools, the CLI's own mode decides them: the gate gives no opinion, or is not asked at all when no deny_tools is set and no grant but git-commit is left.

Workers and the sandbox

A worker's environment is allowlisted, not inherited. It does not take whatever the server's own shell had set. A repository's repos.yaml entry adds what it declares. See Environment variables.

A sandbox bounds what a worker writes, and with a network policy what it reaches.

  • A sandboxed worker cannot move a branch in your repository other than its own.
  • A tool policy Kraft cannot enforce for that harness in the sandbox refuses the launch. Where it is enforced, an allowed_tools list on Claude runs in manual mode, and Claude then runs read-only shell commands such as cat without asking Kraft's gate, if Bash is in the list.
  • Of Kraft's results folder it sees only its own work item's files, read-only, and writes only its own result file. Another item's diff, findings and instructions are not in the container.
  • Git state it plants in its worktree, such as a HEAD naming another branch or a rebase or merge in progress, stops the item for you. Kraft never commits, rebases or aborts over it.

What it reaches.

  • Without a network policy its network is not restricted, and on a cloud VM the metadata address is reachable. Kraft refuses the launch unless the sandbox sets unrestricted_network: true, and kraft admin doctor reports the sandbox either way.
  • With one, the worker has only loopback and reaches out only through Kraft's own proxy, on a channel that is its session's alone. Only the hosts the policy allows are reachable. Loopback, link-local, unspecified and multicast addresses, cloud metadata and the addresses the host's own name resolves to never are. Another non-public address, such as an RFC 1918 one, is reachable only when an allow entry names its host exactly. Refusals are recorded.
  • On Docker Desktop and Podman machine that channel is a mutual-TLS connection, and a session's certificate expires after seven days, so a longer session fails closed. See Network policy.
  • The limit: the allow list is enforced on the requested name and the dialed address. So an allowed name on a shared CDN address can reach other names that address serves (domain fronting). Through an upstream proxy, the upstream resolves the name again, so the checked address is not necessarily the one reached.

A variable listed under credentials never enters the container. It needs a network: policy: a sandbox policy that lists credentials without one is refused when it loads. The container holds a sentinel in its place, and the proxy holds the real value in memory. Other variables are not covered: ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN are forwarded into the container unless you list them.

The proxy injects a credential only for a host the allow list names exactly. A host reached only through a wildcard is a blind tunnel, where the worker sees only the sentinel.

For the credential's host the proxy terminates TLS, with a certificate from Kraft's CA, which such a container trusts. It verifies the real host against the server's own roots plus the sandbox's extra CA, and replaces the sentinel in the declared header. It refuses any request that does not carry the sentinel.

The limits: HTTP/1.1 only, no upgrade to a managed host, and a host that echoed the key in its response would hand it to the worker. See Credentials.

A sandbox covers a workspace's members as it covers the root. Each member is checked out from its connected repository, never from a git directory the worker can write. The files host git trusts for it are read-only in the container. A member Kraft did not check out, or one a worker swapped, stops the item before host git runs in it. See Workspaces.

A Kit runs only what Kraft enforces. A kind: kit sandbox runs a Kit pinned by digest whose descriptor decodes strictly. Kraft lowers the Kit onto the same Docker sandbox: its network lists, credentials and limits alone, with no harness host added.

  • A required capability Kraft does not enforce stops the item naming it before anything runs, and an optional one is skipped and recorded.
  • A Kit credential's value comes only from the server variable sandbox.yaml binds to its service, never from a variable the Kit names.
  • Kraft reads one Kit you wrote. A Kit that needs a capability Kraft does not enforce is refused, as Docker's published ones are. See Kits.

Files and settings

  • The permission hook imports nothing from the worktree. Cursor runs the hook from the worktree, which the worker writes, with the worker's environment. Kraft starts it with Python's -I, so neither the current directory nor a PYTHONPATH is on its import path. A file in the worktree named like a module Kraft imports cannot run in its place and answer the hook. See Cursor.
    This does not protect Kraft's own install: an unsandboxed worker can change that as it can any file your user owns (see Default posture).
  • Search indexes only files inside the repository. Kraft indexes the Markdown files git tracks under a repository's .engineering/. It skips a symlink, wherever it points, and anything else that is not a regular file. A repository cannot commit a link to a file outside it, such as a token in your home directory, and have search hand that file to you or your agent. The scan also never runs a core.fsmonitor program the repository's own git config names.
  • A worker's session summary is read only from its own worktree. It is read only if it is a Markdown file, so a worker cannot point search at a file such as your checkout's .env.
  • Settings can change what the server runs. A harness profile's executable (Settings › Harnesses) is the program every agent launch on that harness profile starts. A repository's setup_command (Settings › Repos) runs in every new worktree.
    Both are editable by anyone who can open the Settings pages. On a loopback bind, a request from this machine needs no password, so anyone with access to this machine can change them. Off loopback, the access password (Settings › Access) guards command execution, not only configuration. Keep the bind on loopback or behind the access password, and review these fields after anyone else has had access.
  • install.sh is a shell script fetched and piped from the internet. It is short by design: read it before you run it, install.sh.

The bearer token

$KRAFT_HOME/run/mcp-token is a full-admin credential. A request that carries it as Authorization: Bearer <token> passes the login check on every route: it can file, start, approve and cancel work, and change settings. It is not limited to /api/triggers.

$KRAFT_HOME/run/trigger-token is the one to hand out. It passes the login check on POST /api/triggers only, so its holder can file work, which lands paused, and nothing else. On any other route Kraft treats it as no credential and answers 401. The mcp-token works on POST /api/triggers too.

  • Kraft creates both files with mode 0600 on first start and keeps them after that. kraft admin doctor fails its mcp token or trigger token check if the file's mode gives anyone but you any access.
  • The kraft CLI and the MCP server read the file on every call, so they need no setup.
  • To rotate either, follow Rotate the token.
Give CI systems and webhook senders the trigger-token, never the mcp-token. Anyone with the mcp-token has full control of this Kraft instance and of the work it runs in your repos.

Login

Remote access uses a password, stored in access.yaml as a salted scrypt hash. A successful login sets an HttpOnly, SameSite=Lax session cookie. Remote access shows how to set it up.

  • Failed logins are throttled per client address. After 5 wrong passwords from one address within 15 minutes, Kraft answers that address 429 with a Retry-After header. It does so even for the right password, until 15 minutes have passed since the last failure. A successful login clears the count. The counts live in memory, so a restart clears them too.
  • The address is the one Kraft sees. A proxy or tunnel on the same machine can name the real client in X-Forwarded-For. Kraft believes that header only from 127.0.0.1, so a remote caller cannot pick its own address.
  • A tunnel that sends no such header makes the throttle global. Every visitor looks like the same address, so 5 wrong guesses from anyone lock everyone out for 15 minutes. That still guards the password, but use a long random one before you expose the board anywhere.
  • The cookie is Secure over HTTPS. When the login request arrives over HTTPS, or carries the X-Forwarded-Proto: https a tunnel sets, the cookie is marked Secure and the browser never sends it over plain HTTP. A login over plain HTTP, such as loopback or Tailscale, gets a cookie without it.

Reporting a vulnerability

Report vulnerabilities privately, not in a public issue. Use GitHub private reporting, as described in SECURITY.md. Only the latest release gets security fixes: see Supported versions.

Copyright © 2026