fix: an empty seeded .credentials.json is a logged-out session that wins - #583
Merged
Merged
Conversation
`dl --claude-profile bear <ws>` forwarded a valid token and `claude` asked the
operator to log in anyway.
`install.sh` seeded `{}` into `.credentials.json` when the file was missing.
Claude Code reads the credentials file before the environment, so an empty one
is not a placeholder: it is a logged-out session, and it beats the
`CLAUDE_CODE_OAUTH_TOKEN` that `dl` had just gone to the trouble of resolving
from the profile and forwarding over ssh.
Both halves of why this went unnoticed:
The stub existed to give a bind mount a source to cover, from the layout where
this feature mounted nine individual paths under `~/.claude`. The host-side
hook already retired its half of that on exactly this reasoning -- "Seeding an
empty {} over a credentials file was never anything but a way to satisfy a
bind source, and on a host that has never run Claude it is indistinguishable
from a logged-out session" (init-host.sh) -- and the container half was left
behind.
And while this feature mounts the host's real credentials file *over* the
stub, the stub is invisible. So the bug could only appear where the forwarded
token was the container's only login, which is the one configuration the mount
rules out: `forwarded_claude` refuses to forward at all into a `Foreign`
config. It surfaced on a container whose Claude mounts had been removed, which
is what a workspace wanting a profile has to be.
Measured both ways in one container, changing nothing else: with the stub,
`claude` prompts for a login; with it removed, `claude -p` answers on the
forwarded token.
`.claude.json` goes with it, seeded by the same block for the same retired
reason. Claude Code creates both itself on first use, so there is nothing to
replace either with.
`test_the_feature_seeds_no_empty_credential` asserts it over the whole
installer rather than about one line, so it cannot return under another name.
There was a problem hiding this comment.
Sorry @JSmithRobotics, you've used your own review budget of 250,000 diff characters for the last 7 days.
You can request another review in 6 days and 19 hours by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideRemoves the installer’s empty Claude config-file stubs, which could override a valid forwarded Sequence diagram for forwarded Claude authentication without seeded credentialssequenceDiagram
participant DL
participant Container
participant Claude
DL->>Container: Set CLAUDE_CODE_OAUTH_TOKEN
Container->>Claude: Start claude
Claude->>Claude: Read credentials configuration
alt Empty .credentials.json exists
Claude-->>Container: Prompt for login
else No seeded credentials file
Claude-->>Container: Authenticate with CLAUDE_CODE_OAUTH_TOKEN
end
Flow diagram for removing empty Claude config stubsflowchart LR
Installer[create_claude_directories]
Files[.credentials.json and .claude.json]
Test[test_the_feature_seeds_no_empty_credential]
Claude[Claude Code]
Installer -->|creates directories only| Claude
Installer -.->|does not seed| Files
Test -->|asserts both files are absent| Installer
Claude -->|creates files on first use| Files
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…-stub-defeats-token-forwarding # Conflicts: # test/unit/test_devcontainer_manifest.py
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
`test_the_feature_seeds_no_empty_credential` claimed to hold "over the whole
installer ... so it cannot come back under another name" while matching only a
line that spelled the filename *and* contained `>`, `tee` or `cp `. Three
ordinary re-seedings walked straight through it:
touch "$TARGET_HOME/.claude/.credentials.json"
install -m 600 /dev/null "$TARGET_HOME/.claude/.credentials.json"
cred="$TARGET_HOME/.claude/.credentials.json" # redirect on the next line
The first is not contrived. `.devcontainer/claude-code/README.md` prescribes
`touch ~/.claude/.credentials.json` for this exact file, so it is the likeliest
way the stub comes back, and the guard written to stop that was blind to it.
The third is what someone does *while* editing the function: factoring the path
into a variable splits the name and the redirection across two lines, and
neither line matched.
The filter was also over-broad the other way, tripping on a `stat` or an
`echo ... >&2` that only named the file. Both halves go: the predicate is now
"named outside a comment", the installer mentions both files only in comments,
and a future line that genuinely needs to name one is meant to be a
conversation rather than a match the filter happens to allow.
Extracted as `_seeding_lines` so the spellings are exercised directly. Those
five parametrized cases fail against the old predicate (3 of 5) and pass now;
the guard itself is still red against the pre-fix installer, and now names all
three of its lines instead of one.
"Claude Code creates both files itself on first use" was true of
`.credentials.json` and wrong about `.claude.json`, which hid the more
interesting half of removing that stub.
`provision.rs` seeds {"hasCompletedOnboarding":true} into
$CLAUDE_CONFIG_DIR/.claude.json and exits early when the file is already there.
This feature sets CLAUDE_CONFIG_DIR to /home/vscode/.claude -- the directory
`create_claude_directories` is populating -- so the `{}` stub satisfied that
guard before the provisioner ever looked. Every container built from this
feature skipped the onboarding seed and met its operator with the trust prompt,
which is precisely what the seed exists to prevent. Removing the stub un-blocks
it, so this change fixes two things and the comment described one.
`test_the_installer_may_not_create_the_file_that_marks_claude_onboarded` holds
the coupling rather than restating it: the filename the provisioner exits on is
read out of provision.rs and required to be one the installer is forbidden to
write. Confirmed to have teeth by dropping `.claude.json` from the forbidden
set, which fails it.
The comment itself is not test-provable, so it goes in beside the guard.
Two user-visible fixes have landed since 0.33.0 and `## [Unreleased]` was empty for both. "All notable changes to this project will be documented in this file" is the file's own preamble, and a release cut off this section would have shipped notes for neither. The credentials one is this branch's. The ssh agent socket one is #582, which merged into main without an entry -- filed here rather than left for the release commit, so the section a cut promotes is already complete. Nothing enforces this: `test_changelog_frozen` only forbids writing into an already-shipped section, so both omissions were silent.
blooop
deleted the
fix/empty-credentials-stub-defeats-token-forwarding
branch
September 9, 2026 07:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dl --claude-profile bear <ws>resolved the profile, forwarded the token, andclaudeasked to log in anyway.The bug
.devcontainer/claude-code/install.shseeded{}into.credentials.jsonwhen the file was missing. Claude Code reads the credentials file before the environment, so an empty one is not a placeholder: it is a logged-out session, and it beats theCLAUDE_CODE_OAUTH_TOKENthatforwarded_claudehad just resolved from the profile and sent over ssh.Measured both ways in one container, changing nothing else:
The token was verified to arrive on both transports before suspecting the file, by fingerprint rather than by printing it:
(default),bearandbaseforward three differentsha256[0:10]values, overdevpod sshand over the OpenSSH hop alike.Why it went unnoticed, which is the interesting half
Two independent reasons, and both of them are about the mounts.
The stub is invisible while the feature mounts the host's credentials file over it. So the bug could only ever appear where the forwarded token is the container's only login, and that is the one configuration this feature's mounts rule out:
forwarded_claudedeclines to forward at all into aClaudeConfig::Foreignconfig, which is what any mount at, under or above the config directory produces. It surfaced on a container whose Claude mounts had been removed, which is what a workspace that wants to use a profile has to be.The reasoning was already written down, on the other side of the same feature.
init-host.shretired its half of this seeding and said why:The container half was left behind. The stub existed for the same reason the host one did: to give a bind mount a source to cover, from the layout where this feature mounted nine individual paths under
~/.claude.The change
Both seeded files go.
.claude.jsonwas written by the same block for the same retired reason, and Claude Code creates both itself on first use, so there is nothing to replace either with.test_the_feature_seeds_no_empty_credentialasserts it over the whole installer rather than about one line, so the stub cannot return under another name. It fails against the old installer, verified by reverting.devcontainer/with the test in place.Cost
This is a change under
.devcontainer/, so the prebuild tag moves and launches build locally untildevcontainer-prebuild.ymlrepublishes onmain. Same caveat as #582, which is open and green.Not in this PR
The mounts themselves. Removing them is what let the forwarded token be used at all, and it is not a fix worth shipping as written: a container with no
~/.claudemount loses the sharedagents/,commands/,hooks/,skills/andwf-skills/, which is the point of the feature. Worth noting that removing only the whole-directory mount would not help either, since a mount under the config directory convicts exactly as the directory mount does.The two shapes that keep both, for a separate discussion:
~/.clauderead-only at a neutral path and copy the instruction directories in frompostCreateCommand. Nothing is mounted at or under the config directory, so it readsOurs. Costs live propagation, which the directory mount was chosen to preserve..credentials.json. A read-onlyskills/mount does not own the login. Costs care: the descendants scan exists because this feature once mounted.credentials.jsonindividually.Happy to open that as an issue instead if you would rather keep it out of a PR thread.
Summary by Sourcery
Prevent stale Claude configuration stubs and incorrect SSH agent socket paths from breaking profile authentication and devcontainer startup.
Bug Fixes:
Tests: