Skip to content

fix: the host ignored $CLAUDE_CONFIG_DIR and read ~/.claude regardless - #565

Closed
JSmithRobotics wants to merge 1 commit into
blooop:mainfrom
JSmithRobotics:fix/claude-config-dir
Closed

fix: the host ignored $CLAUDE_CONFIG_DIR and read ~/.claude regardless#565
JSmithRobotics wants to merge 1 commit into
blooop:mainfrom
JSmithRobotics:fix/claude-config-dir

Conversation

@JSmithRobotics

@JSmithRobotics JSmithRobotics commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

A host that has moved its Claude configuration now forwards its login, instead of
reporting itself as not logged in.

Claude Code reads $CLAUDE_CONFIG_DIR before ~/.claude, and the probe dl
runs inside a container has always read it too
, because a devcontainer feature
may set it. Only the host side did not: it joined a hardcoded
.claude/.credentials.json onto $HOME, so a host that had moved its
configuration returned NoToken::NotLoggedIn while holding a perfectly good
login, and claude in every workspace asked for a login.

The symptom was silent, which is why it survived. "No credential file at
~/.claude" is also the ordinary macOS state, where the login lives in the
keychain, so it is deliberately not warned about. A host that had moved its config
got that same quiet answer, with nothing to tell the two apart.

The variable replaces the default rather than being tried ahead of it

Claude Code does not fall back from $CLAUDE_CONFIG_DIR to ~/.claude, and a
fallback here would forward a credential out of a directory Claude Code is not
reading. That is the same defect one level down: invisible until the host holds two
logins, at which point it forwards the wrong one. An empty value counts as unset,
matching domain::xdg's rule and what a shell exporting a bare variable means.

Order is unchanged above the new arm: DEVLAUNCH_NO_CLAUDE_TOKEN first, then any
exported CLAUDE_CODE_OAUTH_TOKEN, and only then the file. The exported token
stays above the variable because both are ambient, and that hatch is what lets a
dl running inside a workspace forward the token it was handed.

One split worth naming

CREDENTIALS_RELPATH becomes CONFIG_RELPATH plus CREDENTIALS_FILENAME, so the
directory half is a decision and the filename half is a constant.

CONFIG_RELPATH is deliberately not shared with flows::provision's
CLAUDE_CONFIG_RELPATH: it is the same string about two different machines, and a
comment in each names the other. Sharing it would couple the host's layout to the
container's, which is exactly the coupling this bug came from.

Scope

clients/claude.rs plus the README, docs/workspace-tools.md and the changelog.
Public-API snapshots are untouched and verified so, because every item involved is
pub(crate).

Independent of #564. feat: --claude-profile forwards a named Claude login builds
on the config_dir() this introduces and carries this commit until this merges;
once it does, that PR's diff collapses to its own two commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AdSFnBdxie6TosHVmjLY28

Summary by Sourcery

Honor Claude Code’s configured host directory when resolving credentials and forwarding logins to workspaces.

Bug Fixes:

  • Honor CLAUDE_CONFIG_DIR when forwarding host Claude credentials, preventing authenticated hosts with relocated configurations from being treated as logged out.

Enhancements:

  • Match Claude Code’s credential lookup semantics by using the configured directory instead of ~/.claude, while preserving existing opt-out and exported-token precedence.
  • Clarify host configuration and Claude authentication behavior in the README and workspace-tools documentation.

Documentation:

  • Document CLAUDE_CONFIG_DIR, its replacement semantics, credential lookup order, and empty-value behavior.

Tests:

  • Add coverage for relocated configurations, replacement versus fallback behavior, precedence rules, and credential-file token handling.

Claude Code reads $CLAUDE_CONFIG_DIR before ~/.claude, and the probe dl runs
inside a container has always read it too, because a devcontainer feature may
set it. Only the host side did not: it joined a hardcoded
.claude/.credentials.json onto $HOME, so a host that had moved its
configuration reported NoToken::NotLoggedIn while holding a perfectly good
login.

The symptom was silent, which is why it survived. "No credential file at
~/.claude" is also the ordinary macOS state, where the login lives in the
keychain, so it is deliberately not warned about; a host that had moved its
config got that same quiet answer and no way to tell the two apart.

The variable replaces the default rather than being tried ahead of it. Claude
Code does not fall back from $CLAUDE_CONFIG_DIR to ~/.claude, and a fallback
here would forward a credential out of a directory Claude Code is not reading,
which is the same defect one level down: invisible until the host holds two
logins, at which point it forwards the wrong one. An empty value counts as
unset, matching domain::xdg's rule and what a shell exporting a bare variable
means.

Order is unchanged above the new arm: DEVLAUNCH_NO_CLAUDE_TOKEN first, then any
exported CLAUDE_CODE_OAUTH_TOKEN, and only then the file. The exported token
stays above the variable because both are ambient and that hatch is what lets a
dl running inside a workspace forward the token it was handed.

CREDENTIALS_RELPATH splits into CONFIG_RELPATH and CREDENTIALS_FILENAME, so the
directory half is a decision and the filename half is a constant.
CONFIG_RELPATH is deliberately not shared with flows::provision's
CLAUDE_CONFIG_RELPATH: the same string about two different machines, with a
comment in each naming the other.

Public-API snapshots are untouched, since every item involved is pub(crate).
@sourcery-ai

sourcery-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

The host-side Claude credential lookup now honors a non-empty $CLAUDE_CONFIG_DIR exactly as Claude Code does, avoiding fallback to $HOME/.claude while preserving opt-out, exported-token, and access-token-only behavior; tests and documentation cover the lookup semantics.

Flow diagram for host Claude credential resolution

flowchart TD
    A["dl host launch"] --> B{"DEVLAUNCH_NO_CLAUDE_TOKEN set?"}
    B -->|yes| C["NoToken::OptedOut"]
    B -->|no| D{"CLAUDE_CODE_OAUTH_TOKEN parses?"}
    D -->|yes| E["Forward exported access token"]
    D -->|no| F{"CLAUDE_CONFIG_DIR non-empty?"}
    F -->|yes| G["Read CONFIG_DIR/.credentials.json"]
    F -->|no| H["Read HOME/.claude/.credentials.json"]
    G --> I{"Access token found?"}
    H --> I
    I -->|yes| J["Forward access token"]
    I -->|no| K["NoToken::NotLoggedIn"]
Loading

File-Level Changes

Change Details Files
Resolve the host Claude credential directory according to Claude Code’s configuration rules.
  • Capture the ambient CLAUDE_CONFIG_DIR value in host state.
  • Use the configured directory when non-empty, otherwise use $HOME/.claude.
  • Keep opt-out and exported-token precedence unchanged, and continue forwarding only the access token.
rust/devlaunch-core/src/clients/claude.rs
Add focused coverage for configuration-directory selection and credential precedence.
  • Test moved configurations, empty-variable behavior, no fallback to the home directory, exported-token precedence, opt-out precedence, and refresh-token exclusion.
  • Refactor credential path constants into directory and filename components.
rust/devlaunch-core/src/clients/claude.rs
Document the host configuration-directory behavior and its security-relevant semantics.
  • Explain that CLAUDE_CONFIG_DIR replaces rather than precedes the default.
  • Document lookup order, empty-value handling, nested-workspace behavior, and the new environment variable.
  • Record the fix in the changelog.
README.md
docs/workspace-tools.md
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.91304% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.76%. Comparing base (5b7d12f) to head (b2196b3).

Files with missing lines Patch % Lines
rust/devlaunch-core/src/clients/claude.rs 98.91% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 95.04% <98.91%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 95.04% <98.91%> (+0.02%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JSmithRobotics

Copy link
Copy Markdown
Collaborator Author

Superseded by #570, which is the same branch opened from this repository rather than from a fork.

Moved because a cross-fork pull request's base has to be a branch in the base repository, so the stack needed its parents pushed here anyway -- which left three branch names living on two remotes, two of them serving as one PR's head and another's base at the same time. Every update then had to reach both remotes or a diff would quietly misrepresent itself. Same commits, one ref each, and #570 to #573 are now a native GitHub stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant