Secrets and egress
Save provider keys with jazz config set <provider> and enter them at the prompt.
For example, jazz config set openai stores an OpenAI key without putting it in shell history.
For CI or containers, supply the provider’s environment variable. Keep credentials out
of prompts, workflow files, and committed configuration.
This page explains how Jazz stores credentials, redacts tool output, and controls network access.
Where secrets live
Config writes route through the OS keyring, or a chmod 600 $JAZZ_HOME/secrets.json where
there is no keyring.
jazz config set llm.openai.api_key stores the key separately from config.json.
The OS keyring is shared by every Jazz home on the account, so each home files its entries under
its own vault service, jazz.<hash of the home path>.vault. A JAZZ_HOME or --data-dir home
cannot read the keys of another. jazz mcp add --env and --header values are secrets too: they go to the keyring,
and ~/.agents/mcp.json (mode 0600) keeps only their names.
Older per-secret keyring entries migrate into the home’s vault before writes and alongside the first read. A completed migration is remembered for the process lifetime. If a keyring read, write, or deletion fails, the next uncached lookup or mutation retries the incomplete migration; already migrated vault values remain intact. Copying and retiring each legacy entry share the same lock as vault updates, preventing another process from migrating a value after its deletion. Deletion leaves the vault unchanged while migration is incomplete, so a later retry cannot restore a secret that appeared to have been removed.
On macOS, the security CLI alone creates, reads and deletes the vault’s keychain items. The
login keychain asks for your password whenever an item is read by a program other than the one
that wrote it, so keeping one program on both sides means no prompts. Values go to security -i
on stdin, never as command arguments, which other local users can see. security -i refuses
lines over 4,095 bytes, so the vault is split into chunks of up to 3,800 characters, and one
security process reads them all. A write fills a second set of chunks and only then deletes
the first. An interrupted write leaves the previous vault readable.
Jazz 0.16.19 and 0.16.20 wrote each home’s vault through osascript instead, which made the
keychain ask for its password on every start. The first start after updating moves that vault’s
secrets into the new vault and deletes it. It reads the old vault through whichever program
wrote it, with keychain dialogs turned off, so there is no prompt. Only a vault both programs
wrote needs one dialog, and at a local terminal Jazz waits 60 seconds for an answer. Every step
is recorded in ~/.jazz/logs/jazz.log under keyring:.
Read-back is one-way. jazz config show and jazz config get redact every secret, including MCP
env and header values and values merged in from the keyring or the environment; --reveal is the
explicit exception. A token is printed once, when it is minted.
Provider keys can also come from the environment, which is the normal path in a container where no keyring exists. Never put a token in an agent prompt, a workflow file, or committed project config.
The shell environment is scrubbed
execute_command does not inherit your whole environment. A variable is removed before the
command runs when a word of its name marks a secret (SECRET, TOKEN, PASSWORD, PASSWD,
PASSPHRASE, CREDENTIALS, APIKEY, AUTH, PASS, COOKIE, or KEY after another word, as in
APP_KEY), or when it is one Jazz reads a secret from (JAZZ_PEER_TOKEN_*, JAZZ_WEBHOOK_*,
JAZZ_NOTIFY_<TARGET>_WEBHOOK_URL and the other notify target secrets). A model that decides to
env or to shell out to something chatty cannot hand your provider keys or a Discord webhook URL
to it. Words are matched whole, so KEYBOARD_LAYOUT and TOKENIZER_PATH pass through.
When a command needs a secret variable, name it in the agent’s envAllowlist.
Egress is its own axis
A tool that sends data off this machine is marked egress, independently of its risk level.
A read-only tool is not automatically safe. web_search mutates nothing and still hands your
query to a third party. http_request sends what the model chose, to a host the model chose.
This is why egress kicks a read-only tool out of a disclosure tier and into the explicit allow
list for peers and webhooks. “This caller may read” and “this caller may transmit” are different
grants, and the security model keeps them separate.
The tool inventory lists exactly which tools send.
Network egress
HTTP requests and web fetches run without approval by default, including private addresses and
calls after untrusted content. Set global network.httpApproval to a URL array to approve only
matching URLs automatically; unmatched requests ask or park. Approvals authorize one exact call
and never change the list. Redirect URLs must also match the grant, credentials are stripped
across origins, and response byte/time limits still apply. See HTTP approvals.
The private-destination restrictions below apply to other model-selected URL tools, including
read_pdf, rendered pages, and the browser. HTTP tools instead use their
scoped global HTTP authorization.
Other URL tools use the guarded fetch too: read_pdf with a URL, and the HTML rendered by
create_pdf and create_composition. For these tools it enforces four things.
- Public destinations. The hostname is resolved, and every address it resolves to must be
public. Loopback (
127.0.0.0/8,::1), private (10/8,172.16/12,192.168/16,fc00::/7), link-local (169.254/16, which holds the cloud metadata service, andfe80::/10), CGNAT (100.64/10),0.0.0.0, IPv4-mapped IPv6 and the other special-purpose ranges are refused. The URL parser rewrites spellings like2130706433,0x7f000001and0177.0.0.1to127.0.0.1first, so they are refused too. - Every redirect hop. Redirects are followed by hand, at most 20, and each target passes the same check before it is requested. An allowed host cannot bounce a request to metadata.
- Credentials stay with their origin. A cross-origin hop keeps only headers that carry no
credential (
Accept,Content-Type,User-Agentand the like).Authorization,Cookieand any custom header the model set are dropped. A cross-origin 307 or 308 that would resend the body is returned to the model instead of followed. - Bounded bodies. Responses are streamed against the tool’s byte cap and the timeout runs until the body is read, so an endless or slow response costs at most the cap and the timeout.
A URL that reaches this machine or your local network asks for approval instead of failing, and
approving adds the address to the global network.allowPrivateHosts, so the next request goes
through unasked. List hosts ahead of time in the same setting (see
private network hosts):
{ "network": { "allowPrivateHosts": ["homeassistant.local", "192.168.1.0/24"] } }
A hostname entry allows whatever that name resolves to, so list names you control. An address or CIDR entry allows those addresses behind any name. Only the global config file sets it.
Browser safety
The browser tools give the model a real browser, whose pages run their own scripts. Three rules bound it.
- Every request is checked, not just the first. Navigations, images, scripts,
fetch()calls and redirects from the page all pass the destination check above: public addresses only, unless the host is innetwork.allowPrivateHosts. A page on the public internet cannot make the browser calllocalhost, your router, or the cloud metadata address. A navigation the agent starts to a private address asks for approval, likeread_pdf. A link the page opens to one is refused, and the agent has to request that URL directly. With a URL list innetwork.httpApproval, only matching URLs load, plus subresources from the origins the run opened from them.file:URLs are always refused. - Nothing persists. Each run gets a fresh temporary profile, deleted when the run ends. The
browser is closed on completion, error, and interruption. Jazz never attaches to your own
Chrome, so your logins and cookies are out of reach. A
browser.endpointyou configure is the exception: that browser’s host sees what the agent reads and types. - What a page says is data. Snapshots arrive in the
untrusted-contentenvelope and mark the run as having read external content. Afterwardsbrowser_navigateto a URL the run did not receive asks, like other outbound tools.browser_actalways needs approval unless the policy ishigh-risk, and unattended runs park on it.
A password you type with ask_user_secret reaches a page only through browser_act. The
approval names the page, Jazz refuses plain http pages that are not on this machine, and the
model only ever sees the placeholder. The page itself receives the value, so approve it only for
a site you expect to sign in to.
A page that shows a password or payment card field is flagged. browser_act then asks on every
call there, even under the high-risk policy or an allowlist, and the approval carries a warning.
A plugin can add flags through the classify.page hook. A flag only adds scrutiny: a plugin cannot
remove a flag the page’s own structure raised, lower a risk, skip an approval, or clear the
untrusted-content marking.
What this does not cover:
- It is not a sandbox for the page. A malicious page can still try to exploit the browser. Chrome’s own sandbox is on, except as root in a container, where it cannot run. Run unattended browsing as a dedicated user or in a container, as for any tool.
- WebSocket and WebRTC traffic does not pass through request interception, so the destination check does not apply to it.
- DNS answers are cached for a call. A host is resolved once per browser call, so a page that rebinds a name between two requests in the same call is not caught.
- What the agent types goes to the page. A page that looks like the site you expected can collect a typed secret. The approval shows the page address; read it.
- Flags are an extra alarm, not a boundary. The structural check does not see fields inside a frame from another site, and a plugin’s classification reads titles and labels that the page wrote, which can be written to steer it.
Computer use safety (experimental)
The computer tools let the model read and act in desktop apps. They are experimental and run on macOS with Apple Silicon only. Five rules bound them.
- Only apps you granted, and only as far as their class allows. You grant each app by bundle
id, for a time you choose, with
jazz computer grant. Terminals, password managers, System Settings and script editors cannot be granted at all, because controlling one would bypass the shell tool’s approvals or hand the model your credentials or permissions. Browsers can only be read, and editors and Finder take clicks but no typing. The class is fixed by the app and a grant cannot raise it. Grants are read again before every action, so a revoke or an expiry takes effect on the next one. - Enabling authorizes computer use; app grants require consent. Turning on the computer
tools authorizes the capability. The agent can request an app grant in chat for your approval;
jazz computer grantruns from your own terminal. The agent’s file tools ask about every change under$JAZZ_HOME/computerunder every policy. An agent with shell approval under thehigh-riskpolicy can still edit that state, as it can any other Jazz state. - Only a person watching. The tools exist only in a terminal conversation. A script, a schedule, a goal or loop cycle, a webhook, a peer request, a bot, or a detached job does not get them.
- What a window says is data. An observation arrives in the
untrusted-contentenvelope and marks the run as having read external content, so outbound tools stop auto-approving below thehigh-riskpolicy afterwards.computer_inputandcomputer_foregroundneed approval, and a password field accepts only a secret you collected withask_user_secret. Jazz also refuses the shortcuts that empty the Trash, lock the screen or log out, and text that downloads and runs code or deletes data. - You can stop it.
jazz computer stopends the run at its next action and stops the driver at once. One run holds the desktop at a time, and a crashed run releases it.
The first successful session pins the SHA-256 of the cua-driver executable. When the binary
changes, Jazz blocks computer use and offers in-chat approval for the new build. A missing driver
can be installed through an in-chat computer_install_driver approval. The driver runs in its
promptless standard mode with no manifest inherited from your shell.
What this does not cover:
- It is not an operating-system sandbox. An app the agent controls can do anything that app can do, including things Jazz never sees. The class of an app limits what Jazz tries, not what the app does in response.
- The model sees what the window shows. Screenshots and on-screen text go to your model provider. Pixels cannot be redacted: Jazz redacts secrets it knows in the accessibility text, and reads only windows of granted apps, but a window that displays a secret in an image shows it. Keep sensitive apps out of the grant and close their windows.
- The driver’s hash covers one executable. If the driver hands work to a separate helper app, that app is not hashed.
- Text in a window can still steer the model. The envelope and the approvals limit what follows, but they do not stop a model from being persuaded to click something inside an app it was granted.
- The record is metadata.
jazz computer logholds the action, the app and a shortened, redacted element label, never a screenshot or typed text.
Secret values in tool output
Files read normally, whatever they hold: read_file, grep, find, ls, cp and mv treat
.env, secrets.json, ~/.zshrc and ~/.jazz/config.json like any other file. What Jazz holds
back is the secret values inside them. Every tool result passes through one redaction step before
anything logs it or shows it to the model, the transcript or an approver, so file contents, command
output, MCP and HTTP responses, errors, and approval previews are all covered.
Tools that cut their output redact first, so no cut can split a secret into a piece that is no longer recognized:
read_fileredacts the whole file before it takes a line range or applies its character cap, and keeps the file’s line numbers: every line of a private key block shows the placeholder. AsinceByteread redacts the appended text together with the lines before the offset, and an offset inside a line holding a secret returns that whole line redacted.execute_commandandwait_forredact stdout and stderr before capping them at 256 KB, with 16 KB of lookahead past the cap, so a key block that crosses the cap is recognized.list_background_jobsredacts a job’s output before it keeps the tail.grepandedit_file’sreplace_patternmatch against the redacted text. A file holding a secret is searched again through its redacted form, in every output mode, so a pattern probing a value (^DB_PASSWORD=[a-m]withoutputMode: "count") finds nothing, while a search for the name finds the line and shows it redacted. Areplace_patternmatch on a line holding a secret is refused.create_pdfandcreate_compositionserve the local text files their HTML loads (an<iframe src=".env">, a stylesheet, a script) with secret values redacted.read_filedoes not attach a PDF whose text holds a secret; it points the model atread_pdf, whose output is redacted.
Jazz replaces recognized secrets with [redacted:<name>]:
- Values Jazz knows, replaced exactly wherever they appear: every secret setting (provider API
keys, OTLP headers, the daemon and operator tokens, peer and webhook tokens, webhook signing
secrets, notify target secrets), as resolved from the keyring, the environment or the config
file; MCP env and header values whose name marks a secret (
SIGNOZ_API_KEY,Authorization); and every environment variable of the Jazz process whose name marks it as a secret (*_API_KEY,*_TOKEN,*_SECRET,*_PASSWORD,*_KEY,*_PASS,JAZZ_NOTIFY_*_WEBHOOK_URL, …). The placeholder names the setting or variable, for example[redacted:llm.openai.api_key]. Values held only in the keyring are looked up again after a config change and at most once a minute. - Secrets Jazz never saw, recognized by their shape:
NAME=valuelines and YAMLname: valuelines whose name marks a secret (.envfiles, shell profiles,envoutput, compose files, kubeconfig, diff lines, including coloured ones), quoted secret-named literals in JSON, YAML and source ("apiKey": "...", also JSON-escaped),.netrcpasswords, private key blocks (also when the end marker is cut off), JWTs, Discord and Slack webhook URLs,Bearer/Basiccredentials, passwords in URLs, and the key formats of OpenAI, Anthropic, GitHub, Slack, AWS, Google, Stripe, npm and Telegram. A name marks a secret when its last word does (DB_PASSWORD,APP_KEY,refresh_token), soTOKEN_URL,token_typeandmax_tokensare left alone.$VARreferences, templates,<placeholders>, booleans, paths and values shorter than 8 characters (for known values) are left alone. - Structured results by key: in an HTTP response body or an MCP tool’s structured content,
a string under a secret-named key (
access_token,client_secret,password) is replaced whole.
write_file and edit_file refuse text carrying a placeholder that stands for a secret: one the
target file holds, or one Jazz knows. An edit copied from redacted output cannot overwrite the real
value, while placeholder-shaped text that names no such secret, such as documentation of this
feature, is written as it is. write_file also refuses to replace an existing file when the new
content leaves out or changes a line holding a secret, and tells the model to use edit_file on
the other lines instead. The agent can still change a .env file: it edits the lines around a
secret, or replaces a secret line with a new value you asked for.
Jazz’s own configuration is readable like any file, but a write_file, edit_file, mv, cp or
rm that changes it always asks for approval, under every auto-approve policy and allowlist
(yolo included). That covers config.json (or $JAZZ_CONFIG_PATH), a project’s
.jazz/config.json, and everything else under $JAZZ_HOME except the directories that hold
authored content: skills, workflows, personas, memory, workspace, compositions,
generated and webapps. Symlinked spellings count, and so does case on macOS and Windows. With
nobody to ask, the run parks for approval.
Recognition by shape is best-effort. A secret with an unrecognized name and format, or one a shell pipeline transforms before it reaches a tool result (base64, splitting it across lines), is not caught. Shell children also lose credential-named environment variables (see Shell environment), and host isolation remains the job of a dedicated OS user or container (see unattended runs).
Secrets the person types
Some secrets exist only in your head: the password of an encrypted PDF, a one-time code, a disk
passphrase. The agent asks for one with ask_user_secret, and the value never reaches the model:
- You type it hidden. In the terminal, both interfaces draw one
•per character and never show the value, before or after you press Enter. Esc declines.jazz runat a terminal reads it the same way; piped into a run, the first line of stdin is taken as it is. - The model gets a placeholder,
[redacted:<name>](for example[redacted:pdf-password]), and passes it as-is to the argument that needs the secret. - The value is held in memory for the rest of the run, and forgotten when the run ends. While it is held, it is a known secret: every tool result, approval preview and log line has it replaced by its placeholder, whatever its length. It is never written to the transcript, the conversation log, the run record or a provider request.
- Only declared arguments get the value. Just before a tool runs, Jazz puts the value in place
of the placeholder in the arguments that tool declares for secrets:
passwordofread_pdfandpdf_page_count,commandofexecute_command,textofbrowser_act, andtextofcomputer_inputandcomputer_foreground. A command or a typed browser or desktop action carrying one always asks you, under every auto-approve policy, and its approval shows the placeholder.browser_actsends the value off this machine, to the page you are signed in to; see browser safety. The computer tools enter it into an app on this machine; see computer use safety. Any other tool or argument carrying the placeholder is refused, so it cannot reach a file, a URL, a web search, an MCP server, a notification or a sub-agent’s prompt. A placeholder standing for a secret Jazz holds in its config ([redacted:llm.openai.api_key]) is never replaced by anything. - Every typed secret is saved for later runs. The agent must name it (
cloudflare-token); the value also goes to the OS keyring, or$JAZZ_HOME/secrets.jsonwhere there is none, and the name and prompt to$JAZZ_HOME/saved-secrets.json. Every agent on this Jazz home can see the names withlist_saved_secrets; none can see a value. Manage them withjazz secrets. - Loading a saved secret asks you, every time. A later run loads one with
use_saved_secret, which asks you under every auto-approve policy,yoloincluded, before the keyring is read. A run nobody can answer parks instead. Starting the run with--dangerously-allow-reading-secrets(orJAZZ_DANGEROUSLY_ALLOW_READING_SECRETS=1) drops that question, and the one on each command or typed action that uses a secret; those then follow the run’s ordinary approval policy. Use it only for unattended runs you trust with every saved secret. - A placeholder the run does not hold is refused. Passed to a tool that takes secrets, it would
otherwise arrive as literal text. The tool is not run, and the model is told to load the secret
with
use_saved_secretor have you type it. - A parked run asks again. A run nobody can answer (a goal, a loop,
jazz run --park) parks with the prompt and name only. Type the secret in a terminal withjazz runs secret <run id>; it goes to the resumed run in memory. A run resumed for any other reason starts with no typed secrets, so the model loads a saved one or asks again. - In a chat bridge, only a private chat collects one. Telegram, Discord, WhatsApp and iMessage ask for it in a one-to-one chat with the bot and take your next message as the value; that message is never forwarded to the agent as a turn. The Telegram bridge deletes your message as soon as it is read; on the other platforms a bot cannot delete it, so the prompt tells you to delete it yourself. In a group, a server channel or a Photon space, the secret is not collected: the chat is told to use a private chat instead, and the agent is told the same.
Content from outside is labelled
Sub-agent answers retain their external-content provenance while running in the background.
wait_subagents and list_subagents mark results containing those answers or their errors as
untrusted, naming every child whose content is shown.
Results that carry someone else’s words arrive inside an <untrusted-content> envelope that names
the source before and after the text: web_fetch, web_search, http_request, read_pdf URLs,
MCP tools and resources, ask_peer, the output of every execute_command and custom command
tool, and read_file of a file outside the working directory. Command output counts because Jazz
cannot tell what a command read: himalaya printing your inbox and ls look the same from
outside. The system prompt tells the model to read that content as data and to take instructions
only from you. After a run reads external content, egress tools need approval below high-risk;
see unattended runs.
External-content exposure is also recorded as host-owned transcript metadata. Clearing tool
output, automatic or manual compaction, trimming a full context window, the chat’s history cap,
the compaction journal a resumed run reads back, a sub-agent’s answer, a Ctrl+B background task’s
result, a batch stopped by Esc or a timeout, and a transcript returned from a detached host all
carry that restriction forward; saving and resuming that conversation still requires approval for
new destinations under read-only and low-risk.
The model’s summary cannot remove the restriction. Older transcripts still containing an external
tool envelope remain restricted, but exposure already lost from an older compacted transcript
cannot be recovered.
MCP servers are external input
A server definition arrives from outside, including its command, its arguments, and the tools it advertises.
Jazz records whether you trust it separately from the definition itself. Your trust is not part
of the config somebody handed you: it is read only from your global ~/.jazz/config.json, and
applies only to servers defined in your own ~/.agents/mcp.json. A repository’s
./.agents/mcp.json can add servers, which stay untrusted, but cannot replace one of yours or
inherit its trust by reusing its name.
OAuth tokens are bound to the server’s name and URL. A definition that reuses a name with another URL gets no token.
An untrusted server’s tools are not exposed broadly. Treat adding one the way you would treat
curl | sh, because the trust decision is the same shape.
What this does not do
None of it replaces operating-system permissions or network isolation. A shell tool running as your user reaches whatever your user reaches.
Scrubbing the environment stops a key being read out of env. It does not stop a read tool or a
shell command from reading ~/.aws/credentials: the read tools return it with the secret values
they recognize redacted, and execute_command is gated by approval. A secret neither pass
recognizes reaches the model.
If that matters for your deployment, the answer is a dedicated OS user or a container, not a tighter approval policy. See unattended runs.