docs blog library
github

Approvals

In interactive chat, Jazz starts in safe mode: routine reads and low-risk actions run automatically; higher-risk changes ask for approval. Use /mode safe to return to it after enabling yolo.

For scripts and scheduled jobs, choose a policy that permits only the actions the job needs.

HTTP requests and page fetches use global network.httpApproval: automatic by default for all methods and destinations, or one-call approval outside configured URL grants. This policy is independent of risk tiers and previous untrusted content. See HTTP approvals.

Tool availability and approval policy are separate controls.

Availability is whether the tool exists for this agent at all. Removing it means the model never sees it and cannot request it. Approval is whether an available tool may execute without asking.

Risk levels and policies

The approval policy decides which risk levels run without asking: --approval-policy on a run, autoApprove in a workflow, the policy granted when accepting a goal (jazz goal accept <id> --approval-policy <tier>, or the question chat asks as you accept), or the one granted when starting a loop (jazz loop start --approval-policy <tier>, or the question /loop asks).

PolicyRuns without asking
false or unsetNo gated calls. They ask, or are declined when nobody can answer
read-onlyReads, searches, web requests
low-riskAdds memory writes, reminders, triggers, and shell commands judged low-risk
high-riskAdds everything gated: writes, edits, deletes, execute_command

Anything above the active policy is gated: in front of a person it asks, and unattended it is declined or parked. That holds for every tool whose level is above read-only, whether or not it has a proposal half. A plain read-only tool (read_file, web_search) runs under every policy; removing it from the agent is how you deny it. That includes the agent’s own bookkeeping (manage_todos, update_work_state, manage_scratchpad) and spawn_subagent: a sub-agent runs under its parent’s policy, allowlists and tools, so spawning one grants nothing new.

With no policy set, gated calls cannot run unasked. jazz run without --approval-policy, a workflow without autoApprove, a woken trigger or goal without a granted tier: each asks for every gated call, or declines it when nobody can answer.

Interactive chat is different because you are at the keyboard. Its safe mode is the low-risk tier: the classifier runs on each shell command, read-only and low-risk tools and commands proceed, and anything high-risk asks. Chat starts in safe mode, and /mode safe, Shift+Tab and the fullscreen toggle all return to it. Yolo mode is high-risk.

Running fully unattended (yolo)

high-risk (also written true) approves every tool call without asking, shell included. Nothing turns it on for you. Each surface has one explicit switch:

SurfaceHow to opt in
A workflowautoApprove: high-risk (or true) in its WORKFLOW.md frontmatter
jazz runjazz run --agent <id> --approval-policy high-risk "…"
jazz workflow runthe workflow’s autoApprove; --auto-approve runs it headless at that tier
Interactive terminalShift+Tab to yolo mode for this session
A chat bot, one chatsend /mode yolo in that conversation (/mode safe turns it back off)
A chat bot, every chatstart the bridge with JAZZ_APPROVAL_POLICY=high-risk (default low-risk)
A goal or loopgrant high-risk when accepting it (jazz goal accept <id> --approval-policy high-risk)

--auto-approve on jazz workflow run does not raise the tier by itself: a workflow with no autoApprove still runs at false. Before opting in, trim the agent’s tools, set a spend cap (maxCostUSD), and read Unattended runs.

Gated tools act in two phases

An approval-pair tool does not act when the model calls it. The first phase returns a description of what it would do, including a real diff for an edit. Only after approval does Jazz invoke the hidden execute_* half.

File edits show a diff before execution. Declining a proposal leaves the file unchanged.

Only the tool registered for an execute_* half can ask for it. Output from any other tool that is shaped like an approval request (an MCP server’s reply, a fetched JSON document) is refused, and nothing runs. MCP results are always nested under content or structuredContent, so a server cannot produce one at all.

A plain tool above read-only (manage_memory, create_pdf, or a custom command tool) has no proposal half. The executor asks with the tool’s name, level and arguments, and runs the tool once approved.

Shell commands are classified individually

ls and rm -rf / are the same tool, so execute_command cannot have one risk level. Its level is unknown.

When the verdict would change the outcome, Jazz asks a model to classify that specific command as read-only, low-risk, or high-risk, then applies the policy to that.

Before any model is asked, Jazz reads the command the way the shell will and marks it high-risk outright when it contains:

  • an unterminated quote or expansion
  • command or process substitution: $(...), backticks, $((...)), <(...), >(...)
  • a redirection that reads or writes a file (discarding output to /dev/null and 2>&1 are fine)
  • a command name built from a variable ($EDITOR notes.md)

No classifier or policy plugin verdict can lower those. The check is about syntax only and knows no program names: what a particular program does (curl, sh -c, | xargs) is for the classifier or the policy plugin to judge.

Classification follows these rules:

  • Uncertainty is high-risk. Ambiguity resolves upward, never downward.
  • The command is classified first, and the conversation cannot talk it down. A clearly mutating command stays high-risk even if the user asked for something milder.
  • The command text is data, not instruction. It arrives in tagged blocks the classifier is told to ignore instructions inside. In the case that matters, the thing being classified is attacker-controlled.

An unclassified command stays unknown and therefore fails closed.

Narrowing without raising the policy

To permit a specific tool or command, grant a narrow exception:

ControlWhereScope
Per-tool allowlist“Always approve this tool” in an approval promptthis session
Per-command allowlistautoApprovedCommands in ~/.jazz/config.jsonpersisted, execute_command only
Toolset trimmingthe agent’s deniedToolspermanent, and the strongest

Command matching uses a parsed key, never a raw string prefix. Jazz lexes the command the way the shell will, and the key is the binary plus the word right after it when that word is not a flag: git diff --stat keys to git diff, ls -la to ls, git -C repo status to git. An entry covers its own key and any longer key at a word boundary, so git covers git push and git status covers git status --short.

A command that is more than one plain command has no key, and no entry ever approves it. That covers control operators (&&, ||, ;, |, &, a newline), command or process substitution, ${...} expansion, any redirection, a comment, and a leading NAME=value assignment. With git status and git log approved, all of these still ask:

git status && rm -rf x      git status | sh          git status <(rm x)
git status $(rm x)          git status > ~/.bashrc   git status 2>/dev/null
git status `rm x`           git status & rm x        PAGER=x git log

Wrappers are part of the key: sudo apt install x keys to sudo apt and npx jest to npx jest, so approving a command never extends to running it as another user or through a package runner.

With nobody there

An unattended run has no one to ask. It declines gated calls by default and says so, which is usually what you want: the run finishes and reports what it could not do.

When the work genuinely needs a decision, --park saves the run instead, exits 2, and waits. jazz runs show <id> prints what it is waiting on. jazz runs approve <id> finishes it. jazz runs reject <id> --note "why" turns it down with a reason the agent can use.

A resumed run keeps the policy and the --auto-approve-tools list it started with. Answering one approval never widens the rest of the run to the default, and never drops a tier it was granted. It also keeps the rest of its boundary: a tool list a webhook or peer narrowed it to, tools it withheld, and its token, cost and time caps, of which it gets only what earlier segments left.

Over the daemon’s HTTP API, approving or answering a parked run also needs the operator token (X-Jazz-Operator-Token), so an agent that read the daemon token cannot approve its own run. Rejecting needs only the daemon token. See granting authority over HTTP.

Before parking a batch, Jazz gathers its approval requests without running any operations. A configured media companion counts as standing consent, but analysis or generation waits until every call in the batch has its answer. The companion runs once when execution proceeds; checking whether the batch needs approval does not invoke the media companion.

Park only where somebody will actually look. A notify target brings every parked approval to your phone; from a Telegram or Discord bridge the operator can answer it with /approve <runId> or /deny <runId> [why].

machine-readable: /docs/security/approvals.md · /llms.txt · /llms-full.txt