[FEATURE] sandbox credentials mask: substitute the sentinel inside HTTP Basic auth (Base64) so git over HTTPS can authenticate

Status Open
Reported on v2.1.267
Maintainer reply None cached
Activity 0 comments · opened Sep 20, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

I run Claude Code in a dev container with the Bash sandbox enabled, and I mask a GitHub PAT with sandbox.credentials.files (~/.config/gh/hosts.yml, mode: mask, injectHosts: ["api.github.com", "github.com"], network.tlsTerminate: {}) so that sandboxed commands never hold the real token.

This works for gh (gh api user succeeds through the proxy), but it does not work for git over HTTPS. git cannot clone, fetch or push a private repository inside the sandbox. This leaves me without a way to give sandboxed git access to GitHub while keeping the token hidden from the commands Claude runs.

The cause is that HTTP Basic auth Base64-encodes the credential. git (through any credential helper, including gh auth git-credential) sends Authorization: Basic base64(user:password), and the password is the sentinel. The docs say substitution covers headers and request bodies, so the proxy looks for the sentinel as literal text. The sentinel is only present inside the Base64 string, so it is not found, and the sentinel reaches GitHub unchanged.

Evidence, all inside the sandbox, with the same masked token and the same injectHosts entry:

$ gh api user --jq .login                      # token sent as a header: works
<my-login>

$ curl -s -o /dev/null -w '%{http_code}\n' -u "<my-login>:$(gh auth token)" https://api.github.com/user
401                                            # same token sent as Basic auth: fails

$ GIT_TERMINAL_PROMPT=0 git ls-remote https://github.com/<owner>/<private-repo>.git HEAD
remote: Invalid username or token. Password authentication is not supported for Git operations.
fatal: Authentication failed for 'https://github.com/<owner>/<private-repo>.git/'

A trace with GIT_TRACE_CURL=1 GIT_TRACE_REDACT=0 shows git sending Authorization: Basic eXVr.... Decoding that value gives <my-login>:fake_value_..., so the sentinel is there, but only inside the encoded string.

I ruled out the other causes: the failure is a GitHub 401 and not a TLS error, so tlsTerminate and the CA are working; github.com is in both allowedDomains and injectHosts; and I restarted the session after adding it.

Proposed Solution

When a request goes to a host in injectHosts, the proxy should also handle Authorization: Basic <base64> headers:

  1. Decode the Base64 value.
  2. If the decoded user:password contains a sentinel from a mask entry, replace it with the real value.
  3. Re-encode the value and forward the request.

Behavior I would expect around that:

  • No new settings. The existing mask entries and injectHosts apply as they do today, so a setup that already works for gh would start working for git without changes.
  • A Basic header that has no sentinel, or a value that is not valid Base64, is forwarded unchanged.
  • The real value is substituted only on connections that the domain allowlist admits and only for hosts in injectHosts, exactly as for header-based substitution today. The sandboxed command still only ever sees the sentinel.
  • The docs for masking list Basic auth as supported, or state the limitation until it is.

This is comparable to how the proxy already re-signs AWS SigV4 requests after substituting the values: it understands an encoding that carries the credential, so that a tool that authenticates in that form keeps working.

Alternative Solutions

What I tried or considered:

  • gh auth setup-git: registers gh auth git-credential as the helper, and it runs inside the sandbox. It returns the sentinel, and git sends it in a Basic header, which fails as shown above.
  • Bearer / token header through http.extraHeader: git -c http.extraHeader="Authorization: Bearer <sentinel>" and Authorization: token <sentinel> both return 401 on the git endpoint. I could not tell whether GitHub rejects those schemes for git or whether the substitution did not happen, so this gives no positive control.
  • sandbox.excludedCommands for git push / git clone: works, but the command then runs with no sandbox and can read the real token. Options such as --receive-pack and repo-local hooks or config can also run code outside the sandbox, so this undoes the protection I wanted from masking.
  • Making the real token readable in the sandbox (for example through a credential helper that reads a secret file): works, but it defeats masking.
  • Running clone and push on the host: works, but I lose the ability to let Claude push branches from the container.

Priority

Medium - Would be very helpful

Feature Category

Configuration and settings

Use Case Example

  1. I keep a fine-grained PAT (scoped to my repositories, Contents read/write) in ~/.config/gh/hosts.yml, on a tmpfs inside the container.
  2. Managed settings mask that file, with injectHosts: ["api.github.com", "github.com"] and network.tlsTerminate: {}, so Claude's sandboxed commands only see a sentinel.
  3. I ask Claude to clone a private repository and open a branch with a fix.
  4. gh pr create and gh api calls work, because the token goes out in a header.
  5. git clone https://github.com/<owner>/<private-repo>.git fails with Authentication failed, because git sends the token in a Base64-encoded Basic header.
  6. With this feature, step 5 succeeds without any change to the settings, and the token is never visible to the commands Claude runs.

Additional Context

Environment

  • Claude Code 2.1.267
  • Linux, Docker dev container (kernel Linux 7.0.12-linuxkit), sandbox.enableWeakerNestedSandbox: true
  • gh 2.101.0, git 2.47.3

Configuration (excerpt)

{
  "sandbox": {
    "enabled": true,
    "network": {
      "tlsTerminate": {},
      "allowedDomains": ["api.github.com", "github.com"]
    },
    "credentials": {
      "files": [
        {
          "path": "~/.config/gh/hosts.yml",
          "mode": "mask",
          "extract": "oauth_token:\\s*(\\S+)",
          "injectHosts": ["api.github.com", "github.com"]
        }
      ]
    }
  }
}

Technical considerations

  • The current behavior matches the docs ("Substitution covers headers and request bodies"), but the docs do not say that Basic auth is out of scope. The masking section uses ~/.config/gh/hosts.yml and .netrc as examples, and consumers of both, such as git and curl with .netrc, send Basic auth.
  • Other tools that send a credential as Basic auth, such as npm _auth and Docker registry auth, are likely affected in the same way. I have only tested git and curl.
  • There is no startup warning and claude doctor does not report it, so the failure looks like a wrong token.

Related issues

  • #77084 asks for broker-style injection ("agent can use secrets, never read them"). It does not mention Basic auth.
  • #95135 reports hosts.yml missing from the sandbox when denyRead covers ~/.config/gh/. That is a separate problem: in my setup hosts.yml is visible and holds a sentinel.

View original on GitHub ↗