Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **`$CLAUDE_CONFIG_DIR` is now honoured on the host, so a host that has moved its
Claude configuration forwards its login instead of reporting itself as not logged
in.** Claude Code reads that variable 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, and the symptom was silent: `claude` in every
workspace asked for a login while the host was authenticated, with nothing to say
why, because "no credential file at `~/.claude`" is also the ordinary macOS state
and so is deliberately not warned about.

The variable **replaces** `~/.claude` rather than being tried ahead of it, which
is how Claude Code treats it. Falling back would forward a credential out of a
directory Claude Code is not reading, and that is the same defect one level down:
invisible until the host has two logins, at which point it forwards the wrong one.
An empty value counts as unset, and the value is read as bytes rather than as
text, so a configuration directory whose name is not valid UTF-8 is opened as
named instead of being mangled into one that cannot be found. An exported
`CLAUDE_CODE_OAUTH_TOKEN` still wins over both, so a `dl` inside a workspace keeps
passing its own token further down, and `DEVLAUNCH_NO_CLAUDE_TOKEN` still beats
everything.

## [0.29.0] - 2026-09-02

### Added
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,9 @@ anything to its `devcontainer.json`.
- **Your Claude login.** `claude` starts in the container without asking you to log in again. The
host's access token is forwarded as `CLAUDE_CODE_OAUTH_TOKEN`, only into the sessions `dl` itself
opens, so a `postCreateCommand` from a repo you did not write never sees it. Nothing is written
to the container's disk. A repo whose own devcontainer bind-mounts `~/.claude` is detected and
left alone. `DEVLAUNCH_NO_CLAUDE_TOKEN=1` skips it. A workspace that existed before this feature
to the container's disk. The token is read from `$CLAUDE_CONFIG_DIR` when your host sets one and
from `~/.claude` otherwise, which is the order Claude Code itself reads them in. A repo whose own
devcontainer bind-mounts `~/.claude` is detected and left alone. `DEVLAUNCH_NO_CLAUDE_TOKEN=1` skips it. A workspace that existed before this feature
needs one `dl <workspace> up` before it picks the login up, and so does a workspace rebuilt by
a `devpod up` devlaunch did not run; workspaces created since do not.
- **`gh` and `claude` on `PATH`.** If the image has them, nothing happens. If not, `dl` streams
Expand Down Expand Up @@ -416,11 +417,12 @@ Images are yours: `docker system df` is what shows those.
| `HERDR_AGENT=<agent>` | Written, not read: an `aid` launch that starts an agent, or a `dl <ws> -- <agent>` whose command is one, names it here so a session manager can see it. See [docs/workspace-tools.md](docs/workspace-tools.md) |
| `DEVLAUNCH_TIMING=1\|json` | Write a timing summary to stderr. See [docs/performance.md](docs/performance.md) |
| `DEVPOD_SSH_CONFIG=<path>` | devpod's own, honoured rather than set: it is where `devpod up` publishes host aliases, so it is where `dl` looks for them. See [docs/cli.md](docs/cli.md) |
| `CLAUDE_CONFIG_DIR=<path>` | Claude Code's own, honoured rather than set: it is where the host keeps its Claude configuration, so it is where `dl` reads the login to forward. It replaces `~/.claude` rather than being tried before it, exactly as Claude Code treats it. See [docs/workspace-tools.md](docs/workspace-tools.md) |

Every switch here reads the same values: anything but empty, `0`, `false` or `no` counts as
set. On a "no" variable that means turn it off; on an opt-in one it means turn it on. Five
rows are not switches and do not follow it: `DEVLAUNCH_AID_AGENT` and `DEVPOD_SSH_CONFIG`
take a value, `DEVLAUNCH_TIMING` counts only empty and `0` as off, so `false` and `no`
set. On a "no" variable that means turn it off; on an opt-in one it means turn it on. Six
rows are not switches and do not follow it: `DEVLAUNCH_AID_AGENT`, `DEVPOD_SSH_CONFIG` and
`CLAUDE_CONFIG_DIR` take a value, `DEVLAUNCH_TIMING` counts only empty and `0` as off, so `false` and `no`
turn it on, `DEVLAUNCH_AID_REMOTE_CONTROL` takes `1`/`true`/`on`/`yes` or
`0`/`false`/`off`/`no` and refuses anything else rather than guessing, and `HERDR_AGENT` is
the one written rather than read, so a value of your own survives only a line that starts
Expand Down
43 changes: 41 additions & 2 deletions docs/workspace-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ stays in place, including one it was given before you set
## Claude authentication

`claude` starts in every workspace `dl` opens without asking for a login. The
host's access token is read from `~/.claude/.credentials.json` and forwarded as
`CLAUDE_CODE_OAUTH_TOKEN`. Only the variable's name reaches a command line, which
host's access token is read from its Claude configuration directory, which is
`$CLAUDE_CONFIG_DIR` when the host sets one and `~/.claude` otherwise, and
forwarded as `CLAUDE_CODE_OAUTH_TOKEN`. Only the variable's name reaches a command line, which
is the discipline the GitHub token keeps too, by a different route: that one is
staged in a private file because `devpod up` needs it, and this one rides
`--send-env` on the session, so the value travels in the environment and no file
Expand All @@ -72,6 +73,44 @@ working status line; a repo with no `.devcontainer/` at all had neither, and the
reported symptom was a blank status bar in a workspace where `claude-statusline`
was installed and the `settings.json` naming it had never been applied.

### Which credential, on a host that has moved it

`$CLAUDE_CONFIG_DIR` is Claude Code's own name for where its configuration lives,
and `dl` honours it rather than setting it, the way it honours `DEVPOD_SSH_CONFIG`.
The full order a launch reads:

1. `DEVLAUNCH_NO_CLAUDE_TOKEN`, which forwards nothing at all.
2. Any `CLAUDE_CODE_OAUTH_TOKEN` the host has already exported. This is what lets a
`dl` running inside a workspace pass the token it was given further down, so a
workspace can launch a workspace.
3. `$CLAUDE_CONFIG_DIR/.credentials.json`, or `~/.claude/.credentials.json` when
the variable is unset. One step, not two: see below.

**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 neither does
this. A fallback would forward a credential out of a directory Claude Code is not
reading, which is the same defect as ignoring the variable and harder to notice,
because it only shows itself on a host with two logins. So a `$CLAUDE_CONFIG_DIR`
that names a directory holding no credential is a host that is not logged in, and
the launch forwards nothing rather than the other account. It does that silently,
which is deliberate and is the same silence a missing `~/.claude` gets: on macOS
the credential lives in the login keychain and no file is the ordinary state, so a
warning there would fire on every correctly configured Mac.

An empty value counts as unset, which is what a shell exporting a bare variable
means and the rule the XDG directories already follow here. The value is read as
bytes rather than as text, so a directory whose name is not valid UTF-8 is opened
as named.

The exported token stays above the variable because both are ambient, and the
nested-workspace case has to keep working when no variable is set.

This closes an asymmetry rather than adding a feature. The probe `dl` runs inside a
container has always read `$CLAUDE_CONFIG_DIR`, because a devcontainer feature may
set it and Claude Code honours it there too. The host side read `~/.claude`
regardless, so a host that had moved its configuration reported itself as not
logged in while holding a perfectly good login.

### A variable, not the credential file

Claude Code authenticates from `CLAUDE_CODE_OAUTH_TOKEN` alone, with an otherwise
Expand Down
Loading
Loading