Safety
Blitz’s guardrails are layered: approvals and permission rules decide what may run, the file sandbox and the OS sandbox limit what it can reach when it does, and checkpoints and the audit log let you see and undo what happened. Sandboxing and trust explains the design. Specs: approvals, file tools, shell.
File edits show a coloured diff before you approve them. Answer y for once, s for the rest of the session, a for always (saved to ~/.blitz/approvals.json), or n for no. Commands are remembered by their exact text within a workspace, edits per workspace, web requests per host, and MCP tools per server and tool. With no one to ask, sensitive actions are denied unless allowed in the settings.
In plan mode, with /plan, or when the agent chooses to plan first, it presents a plan for approval: approving it carries it out, and typing feedback makes it revise. [blitz] plan_review sets when plans are required: always, agent-decides (the default) or never. For work with several steps, the agent keeps a task list, shown as a checklist that updates as it goes.
| Mode | What runs without asking |
|---|---|
default |
Reads; everything else asks |
accept-edits |
File changes in the workspace too; commands still ask |
auto |
A reviewer model decides what would ask you, with a reason; ask rules still ask you |
plan |
Every prompt is planned; tools that change anything are refused |
dont-ask |
Anything that would ask is refused instead (for CI and scripts) |
bypass |
Everything; only while the OS sandbox is active, and deny rules, blocked paths and the sandboxes still apply |
A workspace starts in auto unless you choose another with --permission-mode, [blitz] permission_mode, or /mode in a session. To restrict one workspace, choose its mode in its settings (Settings › Workspaces, Behaviour): it’s kept for that workspace and it opens in it again. A workflow runs in its own mode, but never a looser one than its workspace’s.
In the auto mode the reviewer sees the action (and its diff), the last few messages and what you tell it about your environment, and answers allow or deny. A denial goes back to the agent with the reason, and every decision is in the audit log (auto-allow, auto-deny); its tokens count in /cost. When the reviewer can’t answer (or takes more than two minutes), you’re asked. A small, fast model is enough:
[permissions.auto]
model = "gemini/gemini-4-argon"
environment = "Our repositories are under github.com/acme; pushing branches is fine, pushing to main isn't. Hosts: *.acme.dev."
A project’s .blitz/settings.toml can’t set it. Workers ignore the mode: they get exactly their own permissions.
Rules say what runs without asking (allow), always asks (ask), or never runs (deny), in every mode. Deny wins over ask, and ask over allow:
[permissions]
allow = ["shell(go test *)", "shell(git status)", "write(docs/**)", "web(*.go.dev)"]
ask = ["shell(git push *)", "write(.github/**)"]
deny = ["shell(rm -rf *)", "read(secrets/**)", "mcp(github:delete_*)", "web_search"]
A shell rule names a command with any arguments: shell(ls) covers ls -la, shell(git log) covers git log --oneline (but not git logs). With * or ? it’s a glob over the whole command (shell(go test *), shell(git * --force*)), and re: starts a regular expression that must match the whole command (shell(re:git (log|show)( .*)?)). A command that writes a file through a redirection (ls > out.txt) always asks, whatever allows the command.
Rules live in the global settings, and a workspace’s own add to them: in the desktop app, Settings › Permissions for the global rules and Settings › Workspaces › a workspace’s Permission rules for its own, where a rule is checked as you type it and can be tried on a command before it’s saved. From the command line:
blitz config permissions # the rules, and the built-in ones
blitz config permissions allow 'shell(make)' --workspace # this workspace only
blitz config permissions deny 'shell(git push --force)'
blitz config permissions check 'shell(git log)' 'git log --oneline | head'
blitz config permissions remove 'shell(make)' --workspace
Built-in read-only rules. Unless [permissions] read_only_defaults = false (or blitz config permissions defaults off), these run without asking: ls, pwd, cat, head, tail, wc, stat, du, df, which, grep, rg, diff, git status, git log, git show, git diff, git blame and git rev-parse. They still ask with git … --output or --ext-diff and rg … --pre (which write a file or run a program), and with a redirection to a file. A deny or ask rule of your own overrides any of them; a workspace can turn them on or off for itself.
Kinds: shell(…), checked on every sub-command, through pipes, bash -c and wrappers; write(…), delete(…) and read(…), path globs relative to the workspace (read rules only deny); web(host), search(provider), mcp(server:tool), skill(name), agent(name), or a bare tool name. Claude Code’s spellings (Bash(…), Edit(…)) work too. /permissions lists and changes the rules for the session (--save writes them to the global settings, --workspace to the workspace’s), and --allow and --deny add them for one run. Every rule is checked before it’s saved, in the settings file too.
File tools reach only the workspace, plus sandbox.allowed_paths (read-write; --add-dir <dir> adds one for a run) and sandbox.read_only_paths, enforced with Go’s os.Root: no .. or symlink escapes. sandbox.blocked_paths (by default .env files, keys, ~/.ssh, cloud credentials and more) are never readable or writable, including through symlinks, grep and list_files.
Every shell command is parsed and each sub-command checked: pipes, $(…), bash -c, find -exec, and wrappers such as env, xargs and timeout. Disguises like s\udo or {r,}m are caught. sandbox.commands.deny always wins; if sandbox.commands.allow is set, only matching commands run. This is a guardrail; the OS sandbox is the boundary.
sandbox.shell is required, auto or off; unset, it’s required on Linux and auto elsewhere (BLITZ_SANDBOX_SHELL overrides the settings). Shell commands, forged tools and stdio MCP servers run under Seatbelt on macOS or bubblewrap on Linux: writes go only to the writable roots, temporary and cache directories and shell_writable_paths; blocked paths are unreadable; the network is off when allow_network = false. required refuses to run commands without the sandbox: the workspace doesn’t open, and the error (SANDBOX_UNAVAILABLE from the service, the desktop’s and the CLI’s message, a failed check in blitz doctor) says how to fix it or opt out.
On macOS, commands can’t read inside your personal folders (Desktop, Documents, Downloads, Movies, Music, Pictures), iCloud Drive, Mail, Messages, Contacts, Calendars, Safari’s data or other volumes. So a command that searches the disk skips them instead of making macOS ask whether Blitz may access them. A workspace inside one of them, such as a project in ~/Documents, still works. To let commands read another folder there, add it to read_only_paths. On every system, a search of the whole disk or your home folder (find / -name …, find ~) is refused: agents find programs with command -v.
On Linux, install bubblewrap and allow unprivileged user namespaces. Ubuntu 24.04 restricts them through AppArmor (sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0, or an AppArmor profile for bwrap), and Docker’s default seccomp profile blocks them. The narrowest fix is blitz security fix-apparmor (or Allow bubblewrap in the desktop’s Settings › Service): it installs an AppArmor profile, /etc/apparmor.d/blitz-bwrap, that lets bwrap alone create user namespaces while every other program stays restricted, asking for your password once (blitz security says what it finds; it shows the commands if you’d rather run them yourself). Until then Blitz won’t run commands on Linux; to run them unsandboxed instead, set [sandbox] shell = "auto" in ~/.blitz/.env.toml (/sandbox and blitz doctor then say why it’s off). Settings files that blitz config init wrote before this change say shell = "auto" themselves, and keep it. bubblewrap can only hide paths that exist when a command starts, so a blocked file that a command creates is visible to that same command; macOS blocks it at once.
Background processes never outlive Blitz. Exiting with processes running asks whether to kill them or wait; a second Ctrl+C force-quits. Each process group is guarded so it’s killed if Blitz dies, even by SIGKILL.
Child processes don’t inherit credential variables (sandbox.scrub_env: *_API_KEY, *_SECRET and more). Blitz’s sign-ins stay its own: ~/.config/gcloud is blocked, and so is the ant command’s directory (~/.config/anthropic) unless Claude signs in with it. With [sandbox] share_adc = true (your settings only; a project can’t set it) and a provider on Google Cloud ADC, the agent’s commands get that sign-in: each its own copy of the credentials file, named by GOOGLE_APPLICATION_CREDENTIALS and removed when the command ends, so code that calls Google Cloud works from them. The copy holds a refresh token, which a command with the network could send anywhere: turn it on only for workspaces you trust. The audit log, the diagnostic log and telemetry mask secrets. Sessions, history, approvals and audit files are owner-only.
Before a file tool changes a file, Blitz keeps a copy, grouped by prompt, so /undo can put it back. Checkpoints are kept between runs in ~/.blitz/checkpoints/<workspace>/ (owner-only, stored by content hash), up to [checkpoints] max_bytes (64 MiB) and max_age_days (30). /rewind goes back to before any earlier prompt: its files, the conversation, or both. Only the file tools’ changes are tracked: if a file changed since, by a command or by you, /undo stops and says so (--force overwrites).
~/.blitz/audit/audit-YYYY-MM-DD.jsonl records prompts, tool calls and results, approvals, denials, hook decisions and undos, plus your own ! commands and /search web queries.
web_fetch only reaches public addresses, checked after DNS resolution and on every redirect: no localhost, private ranges or cloud metadata. It needs approval per host unless the host is in web.allow_domains, and caps the response size. The agent’s web_search asks before each query leaves your machine. The browser tool follows the same rules for every site it opens, and all its traffic goes through Blitz, which refuses local and private addresses unless browser.allow_local is set (the browser).