Threat model
Status: draft — v1 to be dated and published. Items marked ☐ are self-audit checks still to be run before any public safety claim.
Jazz’s safety stance is fail closed by construction: when the agent, the classifier, or the operator hasn’t explicitly widened what may run, the answer is “ask a human.” This document lists what that means concretely, what it does not protect against, and how to check both.
What runs without asking
One dial — the approval policy — governs every surface the same way (terminal, CI, bots):
| Policy | Auto-approves |
|---|---|
false | Nothing. Every gated tool asks. |
read-only | Reading files, search, web requests |
low-risk | + todo tracking, reminders, spawning sub-agents |
high-risk | + file changes, shell commands, git commit and push |
Everything above the dial blocks and emits approval_required; the run resumes only when
an approval_decision comes back — from the terminal, or from your phone via a chat
bridge. An unattended run that hits its ceiling waits; it does not die and it does not
proceed. Source: tools-and-approval.
Two sharper controls sit under the dial:
- Toolset omission. An agent’s tool list is explicit; a tool not listed does not exist
for that agent. An agent without
execute_commandcannot run shell commands regardless of policy. This is the strongest control and the recommended one for anything unattended. autoApprovedCommands. A persisted allowlist that admits single commands without raising the whole tier. Matching is a parsed key (binary + first subcommand) with word-boundary comparison —git statusdoes not also permitgit status && rm -rf /. Source:packages/core/src/agent/tools/command-risk.ts, configuration.
Shell commands fail closed
Commands with no static risk annotation are classified before approval. The classifier’s
instruction is explicit: “high-risk = anything else, including uncertainty” and “a
clearly mutating command is high-risk even if the conversation asked for something
milder.” Text inside the command is treated as data to classify, never as instructions.
An ambiguous command on an unattended run therefore blocks rather than runs.
Source: packages/core/src/agent/tools/command-risk.ts.
Where secrets live
- API keys are stored in the OS keyring, not in config files
(
packages/adapters/src/secrets/keyring.ts;JAZZ_DISABLE_KEYRINGopts out). - Child processes are scrubbed. Any environment variable whose name matches
API|KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTHis stripped before a shell command or custom command tool spawns. Exceptions require an explicit per-agentenvAllowlistentry;SSH_*names can never be allowlisted. Source: configuration → envAllowlist. - Known weakness, stated plainly: conversation transcripts are plaintext JSON under
~/.jazz/history/. Treat that directory as sensitive. Anything the agent read during a run may be in there.
Network posture
- The CLI opens no listening port.
jazz runis a process that starts, works, and exits. - The Telegram bridge defaults to long-polling (
getUpdates): outbound connections only, no public URL, works behind NAT. Webhook mode exists but is opt-in and requires a secret. One caveat stated honestly: the bridge container always runs a minimal/healthHTTP endpoint for container health checks. Source:packages/telegram-bot/bridge.ts. JAZZ_OFFLINE=1stops every outbound request Jazz makes on its own behalf except model inference itself. Source: airgapped.
Runaway protection
Unattended runs are budgeted, not trusted: an iteration ceiling with escalating wrap-up pressure, loop detection keyed on tool-name plus arguments, context compaction, and cost reported on every run. Source: agent-loop.
What Jazz does NOT protect against
Claiming less is part of the model. Jazz does not currently defend against:
- Prompt injection steering permitted actions. Content the agent reads (a web page, an email, a PR body) can influence what it does within its approved tier. The mitigations are structural — narrow toolsets, low tiers for unattended runs, approval walls for everything mutating — not content analysis.
- A hostile deployment operator.
customToolscommand handlers run what the deployment configured; they are deployment-authored trust, always registered high-risk. - A compromised model provider. Inference traffic goes to whichever provider you
configured. Local models via
ollamaremove that dependency.
Self-audit checklist (run before every public safety claim)
Failure classes below are the ones that burned other agent deployments in 2026. Each gets re-checked, on the released binary, before we say the word “safe” anywhere:
- ☐ Fresh install: no plaintext key written anywhere under
~/.jazzwhen the keyring is available. - ☐ Bridge
.envhandling: bot token never logged, never echoed into transcripts. - ☐ Default agent toolset after the wizard: confirm
execute_commandposture and that the default approval policy asks before mutations. - ☐
read-onlytier semantics: enumerate exactly which outbound requests it permits, and document that list. - ☐ Port scan of a default
docker compose upbridge: nothing listening except/health. - ☐
jazz bench safetytripwire suite passes 10/10 on the release candidate (planned — see the eval harness). - ☐ Webhook mode: secret required, requests without it rejected.
Reporting
Vulnerabilities: see SECURITY.md. Reports that demonstrate any checked item above failing are treated as release blockers, not enhancements.