nextv1.4.0
Project

Security

Kraft's threat model: what it protects against, and what it deliberately doesn't.

Default posture

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

  • No sandbox. No repo sets sandbox:, so every agent runs directly on your machine, as your user. A sandbox is opt-in per repo; see Sandboxed workers.
  • Your home directory and your logins. A worker's environment is an allowlist, but the list includes HOME and SSH_AUTH_SOCK, and it passes ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN through. A worker can read anything your user can: ~/.ssh, your forge CLI's stored login, your agent's login, and $KRAFT_HOME, including the bearer token. It can push with your SSH agent.
  • Open network. Without a sandbox and a network: policy, a worker can reach any host your machine can.
  • Claude runs in auto with every tool. Every agent task in the shipped chains runs on Claude Code with --permission-mode auto and no tool list. In auto, Claude's own classifier decides most calls without asking Kraft. A call that does reach Kraft's permission gate is allowed, because no allowed_tools is set. Only Monitor and your deny_tools are refused.
  • Gemini and Amp ask nobody. Gemini runs with --approval-mode yolo and Amp asks for no approvals, so neither reaches the permission gate. Gemini cannot enforce a tool list at all and refuses to launch under one.
  • Escalation turns can force-push their item's branch. An escalation turn is the agent Kraft starts to help with a stopped item. Kraft starts one by itself when an item stops for a person somewhere other than a gate (auto_escalate_stuck, on by default, up to three times). The turn holds the git-commit, git-rebase and git-push grants. The push grant covers only a push to the item's own branch, with --force-with-lease but not a plain --force (see Grants), and 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, so it may also retry or resume its own item. It cannot approve or reject that item's gates, or raise or otherwise change its budget or policy: a gate and a spending cap are a person's decision, and the API refuses the escalation's own session. Set defaults.escalation_grants: [] in policy.yaml to remove the grants.
  • The local API has no login. While 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 through it.
  • An agent deciding its own gates. A sandboxed worker reaches Kraft only through its session's channel, which acts as that session and allows only reads, progress, thread replies and permission asks on its own item, and an escalation turn's retry of it. A worker cannot approve, reject, pause, resume, skip, abandon, retry, complete, cancel or escalate its item, revise its attachments, overrides, policy or budget, or file work, whatever it sends. An unsandboxed worker runs as your user: it can read Kraft's token, write Kraft's database and edit its config, so no check inside Kraft can stop it from approving its own gate or starting work it filed. The kraft CLI, the MCP server and the API refuse a worker's approve, reject, pause, resume, skip, abandon, retry, complete, cancel and escalate on its own item, its changes to that item's attachments, agent and node overrides, policy and budget, its review comments and review submissions, and its autostart, when the worker identifies itself. That stops an agent following the normal path, not one working around it. If a gate must hold against the agent, run the work in a sandbox, and protect your default branch with a required review from someone other than the account Kraft uses.
  • Most shipped gates wait for a person. The default chain stops for you to approve the spec and the plan, before it opens a draft merge request, and again before it marks it ready. Two exceptions: a spec or plan you attach at intake arrives already decided, so its gate is dropped; and 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, it then waits for that approval before it merges. A gate you give an auto_review task lets that agent's verdict approve it.

Threat model

Kraft runs on your machine, binds 127.0.0.1 by default, and edits your repos through git worktrees using whatever coding agent you've configured. The things worth being deliberate about:

  • 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.
  • Host allowlist. A non-loopback bind also checks incoming requests' Host header against allowed_hosts in access.yaml — a password alone does not authorize an arbitrary hostname. See Remote access for 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.
  • An agent cannot file work and start it in one step, or approve its own gate. Everything a coding agent files through the MCP tools or kraft item create lands paused; a person starts it, from the board or by asking their agent to resume it. kraft item create --autostart is a person's shortcut past that click, and the server refuses it (403, nothing filed) from a Kraft worker session or an MCP client. A worker session cannot approve, reject, pause, resume, skip, abandon or retry the work item it is itself running. A sandboxed worker cannot get past either refusal; an unsandboxed one can. See Default posture for why, and Agent integration.
  • Worker environment is allowlisted, not inherited. A worker's process environment is built from a fixed allowlist (PATH, HOME, locale, proxy and CA vars, KRAFT_*, the agent's credential var) plus whatever a repo's repos.yaml entry explicitly declares — not whatever the Kraft daemon's own shell happened to have set. See Configuration.
  • 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, and its tool policy is enforced in the container or the launch is refused. 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. Without a network policy its network is not restricted, and on a cloud VM the metadata address is reachable; kraft admin doctor warns about it. 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, and loopback, link-local, cloud metadata and the host's own addresses never are. Refusals are recorded. On Docker Desktop and Podman machine the channel is a mutual-TLS connection to the daemon's loopback, identified by a certificate from Kraft's own CA that only that session's second relay holds, never the worker; it expires after seven days, and a longer session fails closed. The limit: the allow list is enforced on the requested name and the dialled 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. See Network policy. The same channel is the worker's only route to Kraft: a kraft call from inside acts as that session, whatever it sends, and only on its own work item; anything else is refused and recorded. A permission hook that gets no answer denies. See Callbacks from a sandbox. A variable a repository lists under credentials never enters the container: it holds a sentinel, and the proxy, holding the real value in memory, terminates TLS for that credential's host alone (with a certificate from Kraft's CA, which such a container trusts), verifies the real host against the daemon's own roots, and replaces the sentinel in the declared header, refusing any request that does not carry it. 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, and 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, lowered 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 daemon variable sandbox.yaml binds to its service, never from a variable the Kit names. Kraft runs operator-authored Kits; Docker's published ones are refused. See Kits.
  • Settings can change what the daemon runs. A harness profile's executable (Settings → Harnesses) is the program every agent launch on that profile starts, and a repository's setup_command (Settings → Repos) runs in every new worktree. Both are editable by anyone who can open Settings, so the Settings password guards command execution on this machine, 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, stop the server, delete the file and start again. Kraft writes a new token on start. Then update any copy you put elsewhere, such as a CI secret.
    kraft admin stop
    rm "${KRAFT_HOME:-$HOME/.kraft}/run/trigger-token"   # or run/mcp-token
    kraft admin start
    
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.

  • 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, 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 every visitor look like the same address, and then the throttle is global: 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.

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. Without a sandbox, do not point Kraft at a repository, a trigger source or a bead queue whose content you do not trust.

Out of scope

Kraft does not try to protect against:

  • Other users and processes on the same machine. The 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 sandbox. It has your user's access; see Default posture.
  • The agent CLI and its provider. Kraft 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 commands. Its 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 token. Either one grants full control.
  • The machine and the forge. A compromised OS, container runtime, gh/glab login or git remote is outside what Kraft can check.

Reporting a vulnerability

Report vulnerabilities privately, as described in SECURITY.md.

Copyright © 2026