Playbook: Inline credential in a git remote URL — leak prevention & remediation

Trigger: A git remote URL on any checkout contains an inline credential — https://x-access-token:<PAT>@github.com/... or https://user:pass@host/.... Any git command that prints the remote (git remote -v, git config --get remote.origin.url, git config --list, verbose git) then leaks the token verbatim into chat transcripts, agent_tasks.output, and queue result columns. Originating incident: #1994 (bms-4 /opt/p24-infra origin, observed during worker job dev_r_worker_queue:387).

Related: docs/playbooks/static-api-key-incident-rotation.md (the authoritative rotation procedure — start there for the PAT rotation itself).


Why this is dangerous

A repo-write PAT embedded in the remote URL is recoverable from any captured log that ran a remote-printing git command. Worker agents print their command output to agent_tasks.output (Supabase) and to chat transcripts, both visible to more people than a local shell. One git remote -v is enough to expose the token.

Never reference the token VALUE in any output — key NAME / type only.


Layer 0 — Provisioning prevention (worker clones)

scripts/spawn-worker.sh clones each worker checkout from a credential-less URL (https://github.com/<owner>/<repo>.git) and authenticates via the gh credential helper rather than inlining the PAT in the clone URL. After cloning it pins the helper into the new repo’s local config:

git -C "$WORKDIR" config --local --replace-all credential.helper ''
git -C "$WORKDIR" config --local --add credential.helper '!gh auth git-credential'

So the worker’s own git fetch/git push resolve the token through gh auth git-credential (gh’s stored login) and origin never carries a token — no git error in a worker session can echo one. This is why the worker /tmp clones are “credential-helper based” in Layer 1 below. Recurrence fixed in #2295 (original provisioning inlined https://${GH_TOKEN_VAL}@github.com/...). Do NOT re-introduce an inline-token clone URL in worker bootstrap.

Layer 1 — Prevention (guard hook)

.claude/hooks/pre-bash-safety.sh blocks remote-printing git commands when, and only when, a configured remote actually carries an inline credential:

  • Blocked patterns: git remote -v|--verbose|show|get-url, git config --get remote.*.url, git config --get-regexp …remote, git config --list|-l, bare git ls-remote.
  • Detection is boolean (grep -q) — the guard never prints the URL.
  • Clean credential-helper checkouts (e.g. the worker /tmp clones) are unaffected.

To inspect remotes safely while a token is still inline:

git config --get-regexp '^remote\..*\.url$' | sed -E 's#://[^/@]+@#://<redacted>@#'

Fleet-wide audit (find every leaking checkout, never printing a token)

Boolean scan of every .git/config on the host — reports paths only, never the URL. Used in #2399 to find checkouts beyond /opt/p24-infra (five stale worker clones under /home/claude-runner/p24-infra-* were still carrying inline tokens — one a classic ghp_ PAT):

while IFS= read -r cfg; do
  repo=$(dirname "$(dirname "$cfg")")
  if git -C "$repo" config --get-regexp '^remote\..*\.url$' 2>/dev/null \
       | grep -qE '://[^/@]+@[^/]'; then echo "INLINE-CRED: $repo"; fi
done < <(find /opt /tmp /home -maxdepth 4 -type f -name config -path '*/.git/config' 2>/dev/null)

Layer 2 — Remediation (de-inline the remote)

Strip the inline token from the remote and rely on a git credential helper so the token is never stored in the URL or printed:

bash scripts/git-deinline-remote.sh [REMOTE_NAME] [REPO_DIR]
# defaults: REMOTE_NAME=origin, REPO_DIR=current directory

The script is idempotent (a clean remote is left untouched), prints only the redacted URL, and verifies access still works through the credential helper. If the fetch test fails afterwards, configure a helper:

git config --global credential.helper store   # token in ~/.git-credentials (chmod 600)
# or: gh auth setup-git

Preferred: de-inline AND wire the credential helper in one step (#2399)

git-deinline-remote.sh only STRIPS the token — on a checkout that authenticated purely via the inline token (like /opt/p24-infra), stripping it breaks git fetch/git push unless a helper is already configured. Use the combined remediator instead — it de-inlines and pins the canonical !gh auth git-credential helper (the same pattern spawn-worker.sh uses since #2295), then verifies reachability. Idempotent, never prints a token:

bash scripts/setup-git-credential-helper.sh [REPO_DIR] [REMOTE_NAME]
# defaults: REPO_DIR=current directory, REMOTE_NAME=origin

The token is resolved at git-time by gh auth git-credential from gh’s stored login (or GH_TOKEN in the environment — which may itself be minted by scripts/gh-app-token, the GitHub App token that replaced GH_PAT_ADMIN on 2026-06-30). Run it as the user that owns the checkout’s .git/config (for /opt/p24-infra on bms-4 that is root).

On bms-4 /opt/p24-infra (the originating checkout)

/opt/p24-infra/.git/config is root-owned, so this must run as root (an unprivileged worker cannot modify it). Server-operation playbook rules apply — annotate the SSH command with # PLAYBOOK: git-remote-inline-token-leak.md:

ssh root@54.36.123.110 'cd /opt/p24-infra && bash scripts/setup-git-credential-helper.sh /opt/p24-infra origin' # PLAYBOOK: git-remote-inline-token-leak.md

Stale worker clones under /home/claude-runner/p24-infra-* (#2399)

The deprecated pre-#2295 worker layout left abandoned per-job clones at /home/claude-runner/p24-infra-<jobnum>, several still carrying inline tokens. Run the fleet-wide audit above, de-inline any that remain (git -C <dir> remote set-url origin https://github.com/radieu/p24-infra.git), and consider removing the abandoned clones entirely once confirmed stale (no index.lock, old .git mtime, no process cwd inside them).

Layer 3 — Rotation (HUMAN / credential-admin action)

De-inlining stops the leak going forward, but a token that was already exposed in logs must be rotated. Follow docs/playbooks/static-api-key-incident-rotation.md:

  1. Rotate / regenerate the radieu/p24-infra repo-write PAT.
  2. Distribute the new value everywhere it lives — SOPS secrets/n8n-bms4.env.sops (GH_TOKEN), GitHub Actions secrets, live envs, containers, n8n credentials.
  3. Re-point the bms-4 /opt/p24-infra remote at the rotated token via the credential helper (NOT inline) — see Layer 2.
  4. Record the rotation in docs/secrets-rotation-log.md.
  5. Audit existing agent_tasks.output / queue result rows for captured token values where feasible.

Verification checklist

  • Fleet-wide audit (above) reports zero INLINE-CRED: checkouts
  • git config --get-regexp '^remote\..*\.url$' (redacted) shows no user:pass@
  • bash scripts/setup-git-credential-helper.sh reports the remote token-free, helper pinned, and reachable
  • The guard blocks git remote -v on a checkout that still has an inline token
  • PAT rotated and distributed per Layer 3; rotation logged