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

## [Unreleased]

## [0.13.4] - 2026-09-17

### Security
- **Isolated `claude` runs no longer write the claude.ai token to disk.** The
isolation seed copied the whole Keychain credential into a
`.credentials.json` under the per-process `CLAUDE_CONFIG_DIR`, and every
host removed that dir only at process exit, so a parent killed by a signal
(a hook host at Claude Code's hook timeout, an `os._exit`) left the token
copy behind; one machine held 77 such dirs. Claude Code also migrated each
seeded file into a Keychain item named after the throwaway dir, and those
outlived the run too. The core's `claude_isolation_seed` now hands the
access token back as an `env` map, `CLAUDE_CODE_OAUTH_TOKEN`, that every
host sets on the child, and seeds only the account pointer; nothing secret
touches the filesystem, a leaked dir holds no credential, and the child
stores nothing in the Keychain. A `CLAUDE_CODE_OAUTH_TOKEN` already set
on the host process outranks the stored credential, as it does in Claude
Code, so the seed reads no credential source and the child inherits it.
The Python host resolves the token on every run rather than once per
process, so a renewed Keychain token reaches the next run of a long-lived
backend. Claude Code authenticates from that variable without refreshing
it, so a run started after the Keychain's access token expired fails with
its `401 OAuth access token is invalid` instead of refreshing; any Claude
Code session on the machine keeps the Keychain token current.

## [0.13.3] - 2026-09-17

### Fixed
- **Isolated `claude` runs from the default config home find the Keychain
token again.** With `CLAUDE_CONFIG_DIR` unset, Claude Code stores the
Expand Down Expand Up @@ -447,7 +473,10 @@ First release, published to PyPI as `spawnllm`.
generation.
- Click CLI: `spawnllm backends` and `spawnllm call`.

[Unreleased]: https://github.com/yasyf/spawnllm/compare/v0.13.1...HEAD
[Unreleased]: https://github.com/yasyf/spawnllm/compare/v0.13.4...HEAD
[0.13.4]: https://github.com/yasyf/spawnllm/compare/v0.13.3...v0.13.4
[0.13.3]: https://github.com/yasyf/spawnllm/compare/v0.13.2...v0.13.3
[0.13.2]: https://github.com/yasyf/spawnllm/compare/v0.13.1...v0.13.2
[0.13.1]: https://github.com/yasyf/spawnllm/compare/v0.13.0...v0.13.1
[0.13.0]: https://github.com/yasyf/spawnllm/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/yasyf/spawnllm/compare/v0.11.0...v0.12.0
Expand Down
3 changes: 2 additions & 1 deletion conformance/vectors/claude_isolation_seed/account-only.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"content": "{\"oauthAccount\": {\"accountUuid\": \"b\"}}",
"mode": "0644"
}
]
],
"env": {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"content": "{\"oauthAccount\": {\"accountUuid\": \"c\"}}",
"mode": "0644"
}
]
],
"env": {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@
"name": ".claude.json",
"content": "{\"oauthAccount\": {\"accountUuid\": \"a\"}}",
"mode": "0644"
},
{
"name": ".credentials.json",
"content": "{\"claudeAiOauth\": {\"accessToken\": \"tok\"}}",
"mode": "0600"
}
]
],
"env": {
"CLAUDE_CODE_OAUTH_TOKEN": "tok"
}
}
}
3 changes: 2 additions & 1 deletion conformance/vectors/claude_isolation_seed/both-null.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"credentials_json": null
},
"expected": {
"files": []
"files": [],
"env": {}
}
}
11 changes: 4 additions & 7 deletions conformance/vectors/claude_isolation_seed/credentials-only.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
"credentials_json": "{\"claudeAiOauth\": {\"accessToken\": \"kc-tok\"}}"
},
"expected": {
"files": [
{
"name": ".credentials.json",
"content": "{\"claudeAiOauth\": {\"accessToken\": \"kc-tok\"}}",
"mode": "0600"
}
]
"files": [],
"env": {
"CLAUDE_CODE_OAUTH_TOKEN": "kc-tok"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/résumé",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.claude",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/home/testuser",
"claude_config_dir_env": "/home/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct/",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": "https://oauth.example.test"
"claude_code_custom_oauth_url_env": "https://oauth.example.test",
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": ""
"claude_code_custom_oauth_url_env": "",
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/home/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": null,
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "oauth-token-env-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": null,
"claude_code_oauth_token_env": "sk-ant-oat01-inherited"
}
},
"expected": {
"account_path": "/Users/testuser/.claude.json",
"credentials_path": null,
"keychain_service": null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "oauth-token-env-empty-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": null,
"claude_code_oauth_token_env": ""
}
},
"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 @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": null,
"claude_securestorage_config_dir_env": "/Users/testuser/.secure",
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"home": "/Users/testuser",
"claude_config_dir_env": "/Users/testuser/.acct",
"claude_securestorage_config_dir_env": "",
"claude_code_custom_oauth_url_env": null
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"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
"claude_code_custom_oauth_url_env": null,
"claude_code_oauth_token_env": null
}
},
"expected": {
Expand Down
9 changes: 7 additions & 2 deletions go/coreops.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type authProbes struct {

type isolationSources struct {
AccountPath string `json:"account_path"`
CredentialsPath string `json:"credentials_path"`
CredentialsPath *string `json:"credentials_path"`
KeychainService *string `json:"keychain_service"`
}

Expand All @@ -106,7 +106,8 @@ type seedFile struct {
}

type isolationSeed struct {
Files []seedFile `json:"files"`
Files []seedFile `json:"files"`
Env map[string]string `json:"env"`
}

func coreCall(op string, input any) (json.RawMessage, error) {
Expand Down Expand Up @@ -204,6 +205,7 @@ func coreIsolationSources() (isolationSources, error) {
"claude_config_dir_env": nil,
"claude_securestorage_config_dir_env": nil,
"claude_code_custom_oauth_url_env": nil,
"claude_code_oauth_token_env": nil,
}
if dir := configDirEnv(); dir != "" {
host["claude_config_dir_env"] = dir
Expand All @@ -214,6 +216,9 @@ func coreIsolationSources() (isolationSources, error) {
if url, defined := os.LookupEnv("CLAUDE_CODE_CUSTOM_OAUTH_URL"); defined {
host["claude_code_custom_oauth_url_env"] = url
}
if token, defined := os.LookupEnv("CLAUDE_CODE_OAUTH_TOKEN"); defined {
host["claude_code_oauth_token_env"] = token
}
return coreInto[isolationSources]("claude_isolation_sources", struct {
Host map[string]any `json:"host"`
}{Host: host})
Expand Down
4 changes: 3 additions & 1 deletion go/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"maps"
"os"
"os/exec"
"runtime"
Expand Down Expand Up @@ -51,12 +52,13 @@ func runExecPlan(ctx context.Context, plan execPlan, spec RunSpec) (output strin

env := plan.Env
if plan.NeedsClaudeIsolation {
dir, cleanup, e := seedClaudeIsolation()
dir, seedEnv, cleanup, e := seedClaudeIsolation()
if e != nil {
return "", 0, "", false, e
}
cleanups = append(cleanups, cleanup)
env = substituteIsolationDir(plan.Env, dir)
maps.Copy(env, seedEnv)
}

argv := substituteFiles(plan.Argv, paths)
Expand Down
Loading
Loading