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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Isolated `claude` runs from the default config home find the Keychain
token again.** With `CLAUDE_CONFIG_DIR` unset, Claude Code stores the
claude.ai token under the bare `Claude Code-credentials` Keychain item and
suffixes the name with `-<sha256(CLAUDE_CONFIG_DIR)[:8]>` only when the
variable is set, even when it names the default path. The isolation seed
looked up the suffixed name for the default home too, so on a machine with
no `~/.claude/.credentials.json` file every isolated run started without a
token and failed with `Not logged in · Please run /login`. The core now
hands the bare name to the host for the default home and the suffixed one
for a set `CLAUDE_CONFIG_DIR`.
- **The isolated config dir and its credentials file are owner-only from the
moment they exist.** The Rust host created the temp dir with the process
umask (0755 under the usual 022) and every seeded file at 0644, wrote the
token, and only then chmodded it to 0600, so another local user on a shared
tmp could read the token in that window, and a run killed inside it left
the file readable. The Python host wrote the file before its chmod too. Both
now create the dir 0700 and each file with its final mode, `O_EXCL`, before
writing a byte; the Go host already did.
- **The Keychain service name follows Claude Code 2.1.274's rule exactly.**
The core trimmed trailing slashes before hashing `CLAUDE_CONFIG_DIR`, but
Claude Code hashes the NFC form of the variable exactly as set, so `/x/`
named a different item than Claude Code wrote and the run started without
a token. The digest now covers the value as set, NFC-normalized; only the
filesystem paths joined under it are trimmed. A defined
`CLAUDE_SECURESTORAGE_CONFIG_DIR` takes over both the credentials file
location and the digest (empty means the default home and the bare item),
and a set `CLAUDE_CODE_CUSTOM_OAUTH_URL` selects the
`Claude Code-custom-oauth-credentials` items, as they do in Claude Code.
An empty `CLAUDE_CONFIG_DIR` reads the bare item, as an unset one does.

## [0.13.2] - 2026-09-14

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct"
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "config-dir-env-decomposed-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/résumé",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/résumé/.claude.json",
"credentials_path": "/Users/testuser/résumé/.credentials.json",
"keychain_service": "Claude Code-credentials-767ccf48"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "config-dir-env-default-path-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.claude",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.claude/.claude.json",
"credentials_path": "/Users/testuser/.claude/.credentials.json",
"keychain_service": "Claude Code-credentials-1cc69f60"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "config-dir-env-empty-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.claude.json",
"credentials_path": "/Users/testuser/.claude/.credentials.json",
"keychain_service": "Claude Code-credentials"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"host": {
"platform": "linux",
"home": "/home/testuser",
"claude_config_dir_env": "/home/testuser/.acct"
"claude_config_dir_env": "/home/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct/"
"claude_config_dir_env": "/Users/testuser/.acct/",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.acct/.claude.json",
"credentials_path": "/Users/testuser/.acct/.credentials.json",
"keychain_service": "Claude Code-credentials-c157f0be"
"keychain_service": "Claude Code-credentials-101a62ee"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "custom-oauth-url-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": "https://oauth.example.test"
}
},
"expected": {
"account_path": "/Users/testuser/.claude.json",
"credentials_path": "/Users/testuser/.claude/.credentials.json",
"keychain_service": "Claude Code-custom-oauth-credentials"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "custom-oauth-url-empty-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": ""
}
},
"expected": {
"account_path": "/Users/testuser/.acct/.claude.json",
"credentials_path": "/Users/testuser/.acct/.credentials.json",
"keychain_service": "Claude Code-credentials-c157f0be"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": null
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.claude.json",
"credentials_path": "/Users/testuser/.claude/.credentials.json",
"keychain_service": "Claude Code-credentials-1cc69f60"
"keychain_service": "Claude Code-credentials"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"host": {
"platform": "linux",
"home": "/home/testuser",
"claude_config_dir_env": null
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "securestorage-env-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": "/Users/testuser/.secure",
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.claude.json",
"credentials_path": "/Users/testuser/.secure/.credentials.json",
"keychain_service": "Claude Code-credentials-205e9e3d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "securestorage-env-empty-over-config-dir-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": "",
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.acct/.claude.json",
"credentials_path": "/Users/testuser/.claude/.credentials.json",
"keychain_service": "Claude Code-credentials"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "securestorage-env-over-config-dir-darwin",
"op": "claude_isolation_sources",
"input": {
"host": {
"platform": "darwin",
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": "/Users/testuser/.secure/",
"claude_code_custom_oauth_url_env": null
}
},
"expected": {
"account_path": "/Users/testuser/.acct/.claude.json",
"credentials_path": "/Users/testuser/.secure/.credentials.json",
"keychain_service": "Claude Code-credentials-ea269d8c"
}
}
15 changes: 14 additions & 1 deletion go/coreops.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package spawnllm
import (
"encoding/json"
"fmt"
"os"

"github.com/yasyf/spawnllm/go/internal/core"
)
Expand Down Expand Up @@ -197,10 +198,22 @@ func coreAuthProbes(provider Provider) (authProbes, error) {
}

func coreIsolationSources() (isolationSources, error) {
host := map[string]any{"platform": platform(), "home": home(), "claude_config_dir_env": nil}
host := map[string]any{
"platform": platform(),
"home": home(),
"claude_config_dir_env": nil,
"claude_securestorage_config_dir_env": nil,
"claude_code_custom_oauth_url_env": nil,
}
if dir := configDirEnv(); dir != "" {
host["claude_config_dir_env"] = dir
}
if dir, defined := os.LookupEnv("CLAUDE_SECURESTORAGE_CONFIG_DIR"); defined {
host["claude_securestorage_config_dir_env"] = dir
}
if url, defined := os.LookupEnv("CLAUDE_CODE_CUSTOM_OAUTH_URL"); defined {
host["claude_code_custom_oauth_url_env"] = url
}
return coreInto[isolationSources]("claude_isolation_sources", struct {
Host map[string]any `json:"host"`
}{Host: host})
Expand Down
Loading
Loading