Output redaction
When an agent prints a secret, nobody else reads it. Marshal finds credentials in everything a run sends out of its space (its output, its error, its live events and the copies posted to Slack) and replaces each one with a marker before the text is stored or shown. The run page tells you a secret was hidden, your admins get an alert, and the audit log records what kind it was. The value itself never leaves the space.
What you can do with it
- Share run results freely. A deputy that ran
envwhile debugging still posts a clean result to its Slack channel. Everyone in the channel reads the report, and nobody reads the token. - Know when to rotate. A red badge on the run page means the agent printed a credential that Marshal itself put in the space. You know which run did it and what kind of value it was, so you rotate the right key the same day.
- Answer an auditor. Every hidden secret writes a
run.secrets_redactedevent with the run, the place it was found, the kind and the count. The log proves the value was caught without ever holding it.
How it works
Marshal checks the text twice, in two layers:
- Known values. Inside the space, Marshal holds every secret it placed there: your credentials, your HTTP secrets, the run’s own Marshal tokens, and the values its proxy signs in with. It finds each one in every form an agent might print it: as is, base64, URL-encoded, JSON-escaped or inside a JSON document. A match here is certain, so it is the higher severity.
- Known shapes. Credentials that Marshal did not inject are found by their format, such as a GitHub token, an AWS access key or a private key block. The full list is in What is detected.
The first check runs inside the space, on every event before it is streamed and on the result and error before the run reports back. The Marshal API checks the shapes again as it stores the output and serves the events, so every run is covered on its way to every reader. Where both layers match the same text, the known value wins.
Each secret becomes a marker that names its kind:
Using token [REDACTED:github_token] for the API call.
Authorization: Bearer [REDACTED:bearer]
DD_API_KEY=[REDACTED:datadog_key]When a value is found by the name of the variable that holds it, such as DD_API_KEY= or a "private_key" field, the name stays and only the value is replaced. A value Marshal injected becomes [REDACTED:marshal_secret]. Output with nothing to hide is stored exactly as the agent wrote it.
Where it applies
| Where the text goes | What is cleaned |
|---|---|
| The run’s output | Every string in the result, including values inside structured output. Marshal cleans it before storing it, so Show raw and the copy button give you the cleaned output too. |
| The run’s error | The error message and the failure log that comes with it. |
| Live events | The transcript on the run page, the events API and Download log. |
| Slack | The result reply a deputy posts, its questions, and agent requests. The text is cleaned first and shortened after, so a long message never shows the start of a key. |
| MCP clients | runs_logs, runs_wait and runs_attention. Output there also arrives wrapped as untrusted content. See Connect an AI agent (MCP). |
What stays as it is
Redaction cleans the text that leaves a space. The files in your workspace are the agent’s own work and are never changed. Commit SHAs, UUIDs, image digests, hashes, certificates and public keys are not secrets, and they pass through untouched.
Inside a space, the tools that sign in for the agent clean their own output separately: a credential value a tool prints reaches the agent as [redacted-by-marshal]. See A tool prints a marker where a token should be.
Redaction catches secrets an agent prints by accident. It works together with the controls that keep secrets away from agents in the first place: placeholders for HTTP secrets, credentials that go to the tool and not to the agent, network policies and the audit trail. See Let agents use secrets they never see.
See what was hidden
On the run page
A run that hid something shows a badge next to its status, for example 3 secrets were hidden. It never shows a value.
| Badge color | Meaning | Tooltip |
|---|---|---|
| Red | The run printed a value Marshal injected. | This run printed a credential Marshal injected. It was hidden from the output. Rotate it. followed by the kinds |
| Amber | The run printed text that looks like a credential. | This run printed text that looks like a credential. It was hidden from the output. followed by the kinds |
The same flag is on the run in the API, as secrets_hidden with the count, the severity (high or medium) and one entry per hit with its path, layer, kind and count.
In the audit log
Each hidden secret writes a run.secrets_redacted event, one for each place, kind and layer in the run. The event carries the run_id, the path (event, output, error, slack or mcp), the kind, the layer (known_value or shape) and the count. It never carries the value, a part of it or a hash of it.
| Layer | Audit severity |
|---|---|
known_value | Security, shown with a shield |
shape | Warning |
Marshal writes these events itself, so no member is named as the actor. Owners and admins find them with Organization on the Audit log page. Text that is read again, such as an MCP client polling the same logs, is recorded once per run, not on every read.
As an alert
Your org’s owners and admins get the A run printed a secret and it was hidden (admins) alert, once for each run that hid a secret. The row reads, for example, “3 secrets hidden in a run: Nightly dependency check”, adds “(a Marshal-injected value: rotate it)” when the run printed an injected value, and links to the run with Open the run. The alert stays open for 7 days.
Turn on Email me when it is raised on the Settings tab of the Alerts page to get an email such as “A run in Acme printed 3 secrets”, with the run and a reminder to rotate any credential it names. The kind also has a section, Secrets hidden in run output, in the weekly digest. See Alerts.
What is detected
| Kind | What it finds |
|---|---|
marshal_secret | Any value Marshal injected into the space, in any printed form |
marshal_key, marshal_mcp_token, marshal_orch_token | Marshal API keys, MCP tokens and orchestration tokens |
github_token | GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_, github_pat_) |
gitlab_token | GitLab personal access tokens (glpat-) |
anthropic_key | Anthropic API keys |
openai_key | OpenAI API keys |
aws_access_key | AWS access key IDs |
google_api_key, google_oauth_token, gcp_private_key | Google API keys, OAuth access tokens and the private key in a service account file |
azure_client_secret, azure_storage_key | Microsoft Entra client secrets and storage account keys in a connection string |
slack_token, slack_app_token | Slack bot, user and app tokens |
stripe_key, stripe_webhook | Stripe API keys and webhook signing secrets |
atlassian_token | Atlassian API tokens |
linear_token | Linear API and OAuth tokens |
sentry_token | Sentry tokens |
tailscale_key | Tailscale auth, API and client keys |
twilio_key | Twilio API keys |
sendgrid_key | SendGrid API keys |
datadog_key, cloudflare_token, vercel_token | Datadog, Cloudflare and Vercel keys, found by the variable that names them, such as DD_API_KEY, CLOUDFLARE_API_TOKEN or VERCEL_TOKEN |
npm_token, pypi_token, docker_token | npm, PyPI and Docker Hub tokens |
huggingface_token, grafana_token, digitalocean_token, shopify_token | Hugging Face, Grafana Cloud, DigitalOcean and Shopify tokens |
age_key | age secret keys |
jwt | JSON Web Tokens |
pem | Private key blocks (-----BEGIN ... PRIVATE KEY-----) |
bearer | Any other bearer token after Bearer |
Each shape has a minimum length, so ordinary words and identifiers that share a prefix are left alone.
Configure it
Redaction is always on, for every run, with nothing to set up. Each person chooses how they hear about hidden secrets on the Settings tab of the Alerts page: Show this alert and Email me when it is raised apply to this alert, and the Weekly digest switch adds it to the weekly email of all open alerts.