# Jazz threat model

Use this reference when assessing a deployment, giving an agent access to sensitive
data, or exposing it to remote callers. For setup instructions, start with
[Unattended runs](./unattended-runs.md) and [Surface access](./surface-access.md).

Jazz runs model-selected actions as the operating-system user that started it. The harness is
designed to constrain mistakes, prompt injection, over-broad integrations, and unauthenticated
remote callers. It is not a sandbox against a hostile model, compromised dependency, or operator.

## Assets and boundaries

The assets at risk are local files, repository state, credentials, conversation history, model
budget, connected services, and information returned to a remote caller. The relevant boundaries
are independent:

- **Model provider:** receives prompts and attachments sent for inference.
- **Tool execution:** can read, mutate, or transmit data with the Jazz process's authority.
- **MCP and custom tools:** run third-party or deployment-authored code.
- **Plugins:** explicitly trusted in-process modules run with the Jazz process's full OS authority;
  their manifests describe expected use but do not confine code.
- **Remote surfaces:** bots, daemon clients, webhooks, invites, and peers decide who can start work.
- **Persistence:** config, transcripts, work state, logs, and telemetry remain readable to the OS
  account unless the host provides stronger isolation. Jazz writes them owner-only (`0600` files,
  `0700` directories) and repairs an older, wider home once on first start, so other accounts on
  the machine cannot read them ([runtime data](../runtime-data/index.md#permissions)).
  Setgid homes with traversal-only access for other accounts (typically `2751`) are
  operator-managed bridge roots: CLI repair leaves their hierarchy alone and delegates to the
  bridge entrypoint, preserving access to isolated conversation homes and shared personas.

## Controls Jazz enforces

### Capability before approval

An unavailable tool cannot be approved. Jazz resolves the built-in bundle, persona profile, agent
additions, MCP/custom tools, and then applies `deniedTools` and any run-specific allowlist. Use
`deniedTools` for a hard per-agent ceiling; `tools` is additive, not an allowlist.

Available mutating tools use approval pairs: the proposal describes the action, then only the hidden
execution half performs it. `execute_command` is classified per command because its declared risk is
`unknown`. An enabled `classify.command-risk` policy hook may supply that classification and can
therefore affect whether the active tier asks for approval. It cannot classify statically rated
tools, expand the effective tool set, override allowlists or the selected tier, or bypass the shell
denylist. Plugin failure falls back to the built-in classifier; unknown or ambiguous shell commands
remain `high-risk`. The denylist is only defense in depth and is bypassable by obfuscation.

Interactive runs ask a person. Unattended runs auto-approve only what their policy admits and decline
the rest. `--park` is an explicit alternative that persists one waiting run for later approval.

For `edit_file`, the read result carries a digest of the canonical target and full contents. The
proposal rejects a stale digest before presenting a diff, and execution checks it again while
holding a per-file lock before writing. This prevents two Jazz `edit_file` executions from both
applying edits based on the same old contents, including when approval was parked. Other programs
do not honor Jazz's lock; the digest check detects changes already present at execution, but it
cannot make an uncooperative external write atomic with Jazz's write.

The optional LSP plugin starts an operator-configured local language server when an enabled agent
begins work in a matching project. The workspace handler receives the canonical paths of files
Jazz reads or changes and sends their contents to that local server. Its bounded diagnostic summary
is labeled as untrusted data and sent only with the next model request; the host does not treat it
as an instruction or a permission grant. A compromised or malicious server can still put misleading text
in diagnostics and influence the model's reasoning. Semantic queries are read-only tools; mutating
tools declare high risk, show the server's actual text diff through the existing approval gate, and
carry a prepared edit into parked approvals. At execution they recheck complete file snapshots
while holding Jazz's per-file edit locks. Server-initiated edits and executable code action commands
are denied. Multi-file renames are not one atomic transaction, and a configured server runs with the
user's OS authority.

### Disclosure and egress

Risk, disclosure, and egress are separate metadata. A read-only web request can transmit private
text; a local file edit mutates without egress. Webhook and peer runs first enforce a disclosure
ceiling and remove egress tools, then add only tools explicitly named in that caller's `allow` list.
The receiving installation chooses its agent and policy; a peer cannot import the caller's authority.

`http_request` and `web_fetch` are automatically authorized by default, including private
addresses and calls after untrusted content. The operator can set global `network.httpApproval`
to a URL array; unmatched calls ask or park, even at high-risk/yolo tiers. One-call approval
never changes configuration. Each redirect URL must independently match the list or that call's
exact approved URL. All modes strip credential/custom headers across origins, refuse forwarding
bodies across origins, reject embedded URL credentials, and stream response bodies against caps.

Other model-selected URL tools (`read_pdf` and rendered pages) reach public hosts by default.
The guarded fetch checks private addresses and every redirect hop; reaching a private address
requires the global `network.allowPrivateHosts` grant or call approval. A project configuration
cannot widen either global network setting.

Tool results that carry someone else's words (web pages, API responses, search results, MCP output,
peer answers, the output of every shell and custom command, files outside the working directory)
reach the model inside a labelled `<untrusted-content>` envelope, and a standing system-prompt rule
tells it to read them as data. Once a run has read external content, other egress tools stop
auto-approving below `high-risk` for the rest of that run and its sub-agents: they prompt, park, or
are declined. A plain GET of a URL that already appears in the user's messages or in content the run
read, and `web_search` (which only reaches the configured provider), stay automatic. Jazz cannot
tell what a command read, so any `execute_command` run counts as external content: after one,
other egress below `high-risk` needs approval.

A network-backed command-risk plugin is a separate egress boundary. Jazz projects only the bounded
command string into `classify.command-risk`; it does not include conversation history, tool results,
environment variables, or file contents. The manifest must declare command-text egress and the exact
destination, and a local operator must consent to those declarations for the current code digest.

### Secrets

Secrets resolve from environment variables, then the OS keyring, then a mode-`0600` local config
fallback when no keyring is usable. Files read normally; secret values in every tool result
(file contents, command output, errors, approval previews) are replaced before the model, the
transcript, logs or an approver see them: exactly for every secret Jazz holds and every
credential-named environment variable, by shape for `.env`-style assignments, key formats and
private keys. Tools that slice, cap or search their output redact first, so a line range, a
byte offset, an output cap or a `grep` pattern cannot split a secret or probe its value. Writes
putting a placeholder where the target file or Jazz holds a secret are refused, and so is a
`write_file` that would drop a line holding one. Recognition by shape is best-effort and a
transformed secret is not caught; `execute_command` stays gated by approval for that reason.
Edits to Jazz's own config and state under `$JAZZ_HOME` (`config.json`, approvals, agents, tokens)
through the file tools always ask, under every approval policy. Shell children lose variables whose names look credential-bearing
and all `SSH_*` variables unless an exact valid name appears in the agent's `envAllowlist`. Log and
telemetry serializers redact known credential fields. Routine INFO/ERROR logs and shared telemetry
events omit command text, tool arguments, results, and prompt/completion text. The local tool audit
record keeps a bounded argument shape; transcripts and other local records remain sensitive plaintext.
Pending OTLP traces and logs are stored in a private, bounded outbox until delivery or expiry.

The ChatGPT OAuth access and refresh tokens are registered as known secrets for tool-output
redaction. Hosted runs can bootstrap them from `JAZZ_CHATGPT_CREDENTIAL`; Jazz removes that
environment variable before starting child processes and stores the bundle through its normal
private credential backend. A configured `JAZZ_CHATGPT_CREDENTIAL_OUTPUT` receives the rotated
bundle after refresh, so only a trusted workflow should read or persist that file.

### Memory writes

`manage_memory` accepts a new fact or correction only with an exact quote and source ID from a
host-labeled user message. Tool output, fetched pages, model summaries, and a synthetic
compaction transcript cannot mint that source ID. Obvious secret and sensitive claims are
rejected on create or amend, whether they sit in the quoted span, the sentence around it, or the
subject and topic the entry would be filed under. Delete and rename need a quoted sentence that
opens with the request and names the entry; amend needs a quote that names the entry. A hidden
cross-scope source ledger revokes the quoted sentences, as hashed keys, before their memory is
corrected or forgotten, so compaction cannot re-save that statement later. A malformed ledger
pauses cited writes but never blocks a delete; the model cannot address its hidden path through
memory tools. The runner marks terminal messages, a `jazz run` positional
prompt, and an `--input-stdin` frame's prompt as memory sources. A prompt piped on stdin and a
frame's `history` never are, so a webhook body relayed through `jazz run` cannot back a memory
write. Callers that put untrusted
text in the positional prompt are responsible for separating it before invoking Jazz.

Memory discovery and recall skip symbolic links in scope roots, topic directories, and files.
Memory path reads and writes reject linked path components, so a linked external file cannot
be surfaced as a saved preference.

Exact quotation authenticates where words came from, not whether they are a durable personal
fact or which existing entry they should update. A model can still misjudge those questions.
Legacy entries without recorded sentences cannot revoke their original conversation source on deletion.

Memory opportunity receipts retain paths, entry IDs, and hashes rather than transcript or tool text.
Hashes of short facts may still be guessable, so the receipt store remains private under
`JAZZ_HOME`. Forgetting any entry erases the whole scope's receipt window before deleting the
memory file. A new scope epoch prevents an in-flight request from restoring old receipts.
The receipt writer has no path to memory credit, skills, or policy mutation.

### Remote entry points

`jazz run` listens on no port. `jazz daemon` binds loopback by default and provisions an operator
token on first start, including on loopback. If token storage is deliberately disabled and no token
is supplied, a loopback daemon warns and may run without one; a non-loopback bind refuses.

Operator HTTP routes reject browser `Origin` headers, and JSON routes require
`application/json`, reducing drive-by browser access to a loopback daemon. This does not protect
against another local process that already has the daemon token.

An agent can be that process: the daemon token sits in the environment or, without an OS keyring,
in `$JAZZ_HOME/secrets.json`. So the routes that grant authority (accept a goal, start or resume a
loop, approve or answer a parked run) also need an operator token kept only in the OS keyring,
minted only outside an agent-started process, and never loaded into config. A daemon an agent
started grants nothing. What this does not stop: an agent with a shell can query the keychain
itself, but such an agent already holds more than any grant could add.

Peers authenticate with separate per-peer tokens. Webhooks use per-door bearer tokens or an
HMAC-SHA256 signature over the raw body, and remember recent delivery ids and signatures so a
captured delivery cannot be replayed. Both use fixed prompt templates or a fixed peer preamble,
refuse to reveal which webhook names exist, and answer failures without their cause. Their runs
get no operator preferences, no AGENTS.md, and no file attachments from paths in caller text, and
each door has a budget, a concurrency cap, and a body cap. A parked remote run resumes with the
tool list, caller, and remaining budget it parked with, or not at all. Invite secrets are one-time
credentials. Creating an invite without `--host` or `--public-url` discovers the public IPv4
address with a bounded, five-second HTTPS request to `api.ipify.org`; the service sees the
caller IP but receives no Jazz credentials, invite secrets, or questions. Discovery rejects
redirects and non-IP responses. Explicit address flags skip the lookup. Telegram, Discord, iMessage, Photon, and
WhatsApp bridges apply their own sender or conversation allowlists before a run starts.

Bridge reads of sandbox-owned files reject symlinks and nonregular files. They open read
descriptors in nonblocking mode before checking the file type, so a conversation cannot freeze
the bridge by replacing a published composition or another readable store with a named pipe.

## What Jazz does not guarantee

- **Prompt-injection immunity:** hostile content can steer actions already permitted by the toolset
  and active policy.
- **Host isolation:** a shell-capable agent can reach whatever its OS user and network can reach.
  The private-network check covers Jazz's own fetch tools, not programs the shell runs, and output
  redaction catches secrets Jazz knows or recognizes by shape, not one a command transforms first.
  See [Secrets and egress](./secrets-and-egress.md#secret-values-in-tool-output).
- **DNS rebinding:** the guarded fetch resolves a hostname before the runtime connects and
  resolves it again to connect, so a name whose answer changes in between (a zero-TTL rebinding
  record) can still reach a private address. IP-literal URLs and redirect targets are unaffected.
- **Third-party correctness:** an MCP server, custom command, model provider, or chat transport may
  mishandle data after it crosses that boundary.
- **Plugin isolation or preemption:** a trusted plugin can bypass its declared projection, network,
  and secret API; synchronous code can block or terminate Jazz. Cooperative timeouts and disabling
  stop host dispatch, not code that has already escaped host control.
- **Classifier correctness:** an explicitly trusted and consented command-risk plugin can
  misclassify an eligible `execute_command` call. Jazz validates its shape, limits its scope, and
  fails closed on operational errors, but probabilistic judgment is not proof of safety.
- **Air gap from `JAZZ_OFFLINE`:** the flag skips public catalog, library, and update requests;
  it does not block inference, tools, MCP, or OTLP. Enforce egress outside Jazz.
- **Encrypted local history:** transcripts, work state, logs, and local telemetry are files under
  the Jazz data directory.
- **Exact cost and token caps:** iteration, token, and cost limits are checked between
  iterations, so one in-flight call may cross a limit. `maxDurationMs` interrupts the active
  iteration at 85% of its deadline, including model calls and running commands, and the
  final tool-less answer is bounded by the deadline itself.

## Deployment checklist

Before exposing an unattended agent:

1. Remove tools the job cannot need and test the effective list.
2. Use the lowest approval policy that completes the expected path.
3. Scope provider and service credentials to a dedicated account.
4. Isolate the process with a dedicated OS user, container, and outbound network policy.
5. Authenticate and allowlist the actual caller identity; do not treat a public URL as identity.
6. Bound iterations, time, tokens, and cost, then test failure and provider-unavailable paths.
7. Protect `$JAZZ_HOME` as sensitive data and configure OTLP only to an approved collector.

For configuration, see [Approvals](./approvals.md), [Surface access](./surface-access.md),
and [Secrets and egress](./secrets-and-egress.md). Report vulnerabilities privately using
the instructions in [SECURITY.md](../../SECURITY.md).