Claude Code's August 4 build, v2.1.221, extends the sandbox's credential masking from environment variables to files. On Linux and WSL, a sandboxed command can now read a credential file and get back a sentinel decoy — the whole file, or just the spans a regex captures — while the sandbox's egress proxy substitutes the real secret back in only when a request leaves for an allowed host. The command authenticates. The agent, its logs, and anything a prompt injection might trick it into printing never hold the real key. On macOS, file masking isn't available and falls back to a hard deny.

That one-line summary hides a genuinely different security posture, so it's worth taking apart.

Deny protects the secret by breaking the tool. Mask doesn't.#

The sandbox already had a deny mode for credentials. You list a file or an environment variable under sandbox.credentials, and inside the sandbox the file is unreadable and the variable is unset. Safe — and often useless, because the tool that needed the secret now fails. deny on ~/.aws/credentials means the agent can't read your keys; it also means aws s3 sync doesn't run.

mask is the answer to "the tool needs the secret, but the model must never see it." From the docs:

With mask, the sandboxed command sees a per-session sentinel value instead of the real one. When a request leaves the sandbox for one of the credential's injectHosts, the sandbox proxy replaces the sentinel with the real value. The command and anything it logs never hold the real credential, but its requests still authenticate.

Environment-variable masking has existed since v2.1.199. Here's the verified shape, masking a GitHub and an npm token:

{
  "sandbox": {
    "enabled": true,
    "network": {
      "tlsTerminate": {},
      "allowedDomains": ["*.github.com", "registry.npmjs.org"]
    },
    "credentials": {
      "envVars": [
        { "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
        { "name": "NPM_TOKEN", "mode": "mask" }
      ]
    }
  }
}

gh and npm run and authenticate; the process only ever holds GH_TOKEN=<sentinel>.

What 2.1.221 adds: the same trick for files#

Plenty of secrets don't live in environment variables. They live in ~/.aws/credentials, a .npmrc, a service-account JSON, a YAML config with a single API key buried three levels down. Until this release, your only sandbox option for those was deny — block the read and break the tool.

v2.1.221 brings masking to files on Linux and WSL. A sandboxed command reads a sentinel copy of the file: either the whole file replaced with a decoy, or — using an extract regex — only the spans that match, so the rest of the file stays byte-for-byte real and whatever parses it keeps working. The proxy substitutes the real value back on egress, exactly as it does for env vars. Conceptually, a file entry mirrors the env-var one — a path, "mode": "mask", and an extract pattern to pin the masking to just the secret rather than the entire config.

The design goal is narrow and correct: mask the fewest bytes that must stay hidden, so nothing else about the tool's behavior changes.

The one dependency, and why it fails closed#

Masking has a hard prerequisite: network.tlsTerminate. To rewrite a credential inside an outbound request, the proxy has to read that request — which means it has to terminate TLS at the proxy instead of forwarding opaque encrypted bytes. Turn on masking without it and, per the docs, it fails closed:

Without it, masking fails closed: the command still sees only the sentinel, but the sentinel reaches the server unchanged and authentication fails. Claude Code reports this misconfiguration at startup.

Failing closed is the right default — a misconfigured mask produces a clean startup warning and a failed auth, never a real key leaking because substitution silently didn't happen. Note the platform split that follows from the same mechanism: macOS uses Seatbelt and doesn't support file masking, so a mask file entry there degrades to deny. Portable configs should assume that fallback.

Where it fits — and where it doesn't#

Masking is one layer, not a moat. It only protects egress the proxy mediates, so it belongs on top of a deny-by-default network allowlist, not instead of one. The docs are blunt that a broad allowedDomains entry like github.com is still an exfiltration path, because the proxy allows on the client-supplied hostname without deep TLS inspection — domain fronting and similar tricks remain live for high-value threat models.

There's also a deliberate trust boundary worth internalizing: because mask authorizes the proxy to send your real credential to a host, it is honored only from user, managed, or --settings config — never a repository's checked-out .claude/settings.json. A cloned repo can't add a mask rule that quietly ships your token somewhere new. And when the same secret is listed deny in any scope, deny wins.

For a solo founder running Claude Code unattended — on a schedule, across a queue of PRs — this closes a specific, nagging gap. Until now, letting an agent run gh, npm, or aws against real config meant either handing the model live credentials or denying the file and losing the tool. Masking gives you the third option the secrets-management playbook always wanted: the tool authenticates, and the agent holds a decoy. Pair it with short-lived, narrowly scoped credentials and the strict egress allowlist that shipped in 2.1.219, and a compromised or prompt-injected run leaks a sentinel and a locked door — not your keys.