fix(container): writable credentials mount so Claude Code can self-refresh access tokens #202
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#202
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Agent containers bind-mount their credentials directory read-only. When Claude Code's access token expires, it correctly calls Anthropic's OAuth refresh endpoint and gets a new access token — but can't write it back to
.credentials.jsonbecause the mount is:ro. Every subsequent SDK call uses the expired token → 401 → task failure. Only an operator-sideclaude loginrotates the file on disk; the container, which can only read, stays stuck on the expired token.Standalone Claude Code CLI never hits this — it writes the refreshed token back to the same file it read from, all transparent. Containers are the only environment where the write-back step is blocked.
Reproducer
"Failed to authenticate. API Error: 401 Invalid authentication credentials".claude login+just agent-env-syncrewrites the file.Witnessed 2026-04-21T02:12:55Z on task
186bfb5e(dev-default addressing review on PR #199). Review sat unanswered 7 hours until the operator noticed.Root cause
apps/server/src/container.ts::dockerRunmountscredentials_host_dirwith:ro. Read-only blocks Claude Code from writing the refreshed access token back. The next SDK call reads the stale token still on disk and fails.The fix
Mount writable (
:rw). Claude Code inside the container refreshes its own access token, writes it back, carries on. The operator'sclaude loginbecomes an occasional recovery step (for refresh-token rotation), not a routine maintenance task.Acceptance criteria
Mount change
apps/server/src/container.ts::dockerRunmountscredentials_host_dirwith:rwinstead of:ro(or drops the flag; Docker default is rw).claudeuser.config/agents.jsonschema unchanged — the mount flag is a service-level constant, not per-agent.Interaction with
just agent-env-syncagent-env-syncmust not clobber in-flight refreshes. Add an mtime check: only overwrite an agent's.credentials.jsonif the host's is newer. Otherwise skip (agent has a fresher token from self-refresh; no point downgrading).just agent-env-sync --force) still exists for the refresh-token-rotation recovery case.Refresh-token rotation fallback
claude login, the host's refresh token is authoritative; operator runsjust agent-env-sync --forceto propagate.Smoke test
scripts/smoke-creds.shadds a write-probe:docker exec <container> touch /home/claude/.config/claude-code/.probe && docker exec <container> rm /home/claude/.config/claude-code/.probe. Fails loud if the mount is accidentally reverted to read-only.Tests
container.test.tsasserts thedocker runcommand emitted bydockerRunincludes:rw(or no:ro) on the credentials bind.RUN_DOCKER_TESTS=1): start a container, write-probe the mount, confirm the probe succeeds and an actual credentials refresh round-trip works end-to-end.Security posture change (documented)
.credentials.json, but since the same agent can already read + exfiltrate the tokens over the network, write-access does not meaningfully expand the attack surface. Per-agent isolation still holds (each agent writes only its own bind dir).Out of scope
Dependencies
References
186bfb5e, 2026-04-21T02:12:55Z, dev-default on PR #199.Claude Code returned an error result: Failed to authenticate. API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"Invalid authentication credentials"}}read-only; this ticket flips it..credentials.jsonin place. Needs write access.bug(agent-runner): auto-retry once on Anthropic 401 (credential-rotation silent failure)to fix(container): writable credentials mount so Claude Code can self-refresh access tokens