feat(container): bind-mount credentials directory, not file #58
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!58
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/57"
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?
Switch the Claude Code credentials bind from a single file to the containing directory so that
claude loginon the host (which atomic-renames.credentials.jsonto a new inode) is visible inside running containers without a restart.Changes
src/container.ts: adddefaultHostCredentialsDirPath(); replacecredentialsHostPath: stringwithcredentialsHostDir: stringonContainerConfig; updateassertCredentialsBindSafeto check the directory against~/.clauderather than the individual filesrc/webhook-config.ts: resolvecredentialsHostDirfromcredentials_host_dir(preferred) →dirname(credentials_host_path)(backwards compat) →defaultHostCredentialsDirPath()(default)src/agent-runner.ts: passcredentialsHostDirtoassertCredentialsBindSafejustfile(containers-rebuild): readcredentials_host_dir, fall back todirname(credentials_host_path), mount the directory at/home/claude/.config/claude-code:roinstead of the filesrc/webhook-config.test.ts(new): backwards-compat resolution coverage — explicit dir, file-path fallback, neither-set default, priority when both setsrc/container.test.ts: adddefaultHostCredentialsDirPathtests; updateassertCredentialsBindSafetests for directory semantics; update justfile↔container.ts agreement testsrc/agent-runner.test.ts: update fixture to usecredentialsHostDirscripts/smoke-creds.sh(new): manual inode-propagation smoke test — simulates both in-place rewrite and atomic rename, verifies host and container see the same inodeCLAUDE.md: document directory-mount behaviour and smoke-test scriptBackwards compatibility
No config changes required. Existing agents.json with
container.credentials_host_path(file) automatically getdirname(credentials_host_path)as the directory mount source.Closes #57
Review
CI: ✅ green (
#1553, 1m 9s).The core fix is correct and well-structured. Directory bind-mount vs. file bind-mount is the right solution to the inode-orphan problem. Backwards-compat resolution (
credentials_host_dir→dirname(credentials_host_path)→ default) is handled consistently in bothwebhook-config.tsand the justfile.assertCredentialsBindSafenow correctly blocks~/.claudeand any subdirectory. Tests are thorough.One bug blocks approval.
Bug —
scripts/smoke-creds.shexits on the very first counter increment (set -e+ arithmetic)File:
scripts/smoke-creds.shIn bash,
((expr))exits with status 1 when expr evaluates to zero. Withset -eactive (line 17:set -euo pipefail), any command that exits non-zero aborts the script.Both counters start at 0:
The first time
((pass++))is called withpass=0, the post-increment expression evaluates to0(the old value), so the command exits with status 1 →set -ekills the script. Same for((fail++)). The smoke test never completes a single full agent check.This affects three call sites:
{ echo " ERROR: container not found"; ((fail++)); continue; }((pass++))/((fail++))after in-place rewrite check((pass++))/((fail++))after atomic-rename checkFix — append
|| trueto each arithmetic increment (or use+=assignment):Apply the fix to all six
((pass++))/((fail++))occurrences in the script.Everything else is good to go. Fix the smoke-script counter increments and this is ready to merge.
@ -0,0 +46,4 @@'.agents[$n].container.credentials_host_dir // empty' "$AGENTS_JSON")if [[ -z "$creds_dir" ]]; thencreds_file_cfg=$(jq -r --arg n "$name" \'.agents[$n].container.credentials_host_path // empty' "$AGENTS_JSON")set -e+((post-increment))kills the script on the first call when the counter is 0.((fail++))withfail=0evaluates to0→ exit status 1 →set -eaborts immediately. Same for every((pass++))call whenpass=0.Fix:
Apply to all six
((pass++))/((fail++))occurrences in this file.Fixed in
7889958. Replaced all five((pass++))/((fail++))withpass=$((pass + 1))/fail=$((fail + 1))—$((expr))never exits non-zero so it's safe underset -e.Review — APPROVED
CI: ✅ green (run #1560,
success, 56 s on heade37487f)Acceptance criteria (issue #57)
credentials_host_dirconfig key acceptedwebhook-config.tspriority chaincredentials_host_path→dirname()autowebhook-config.tsandjustfile~/.config/claude-hooks/claude-credentialsdefaultHostCredentialsDirPath():ro)--volume "$creds_dir:/home/claude/.config/claude-code:ro"~/.claudestartsWith(\${forbidden}/`)`src/webhook-config.test.ts(new, 7 tests)assertCredentialsBindSafetests updated for directory semanticsdefaultHostCredentialsDirPathtestsscripts/smoke-creds.sh— covers both in-place rewrite and atomic renameCLAUDE.mddocuments directory-mount behaviourCode observations
src/container.ts— The@deprecatedJSDoc ondefaultHostCredentialsPath()is a nice touch; it keeps the backwards-compat path discoverable while signalling intent to callers. The security checkcredentialsHostDir === forbidden || credentialsHostDir.startsWith(\${forbidden}/`)correctly extends the guard to subdirectories of~/.claude`, not just the exact path.src/webhook-config.ts— Resolution priority is clean and the three-branchif/else if/elsemirrors the justfile comment block exactly, which makes the two sides easy to audit together.justfile—${creds_file:+$(dirname "$creds_file")}safely handles an emptyjqresult without passing a baredirname ""to the shell. Good defensive Bash.scripts/smoke-creds.sh— The two-stage simulation (in-placecat src > dstthen atomiccp + mv) precisely reproduces both failure modes from the issue. Note thatstat -c %iis Linux-only (GNU coreutils); since this service runs on a Linux desktop that's fine, but a one-line comment near the top (# Linux only — uses GNU stat -c) would help anyone who tries to run it on macOS.src/webhook-config.test.ts— ThetmpDirs+afterEachcleanup pattern is correct (join(p, "..")normalises to the temp dir, not the file). All three resolution paths plus the priority-when-both-set case are covered.Minor (non-blocking)
scripts/smoke-creds.sh: consider adding# Linux only — uses GNU stat -cnear the top so macOS users aren't surprised. Not blocking.No logic bugs, no unhandled error paths, no security issues, no scope creep. The implementation is complete and correct.