@efoo/ccprofile 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # ccprofile
2
2
 
3
- Per-directory Claude Code account routing via `CLAUDE_CODE_OAUTH_TOKEN`, direnv, and the macOS Keychain.
3
+ Per-directory Claude Code account routing via `ANTHROPIC_AUTH_TOKEN`, direnv, and the macOS Keychain.
4
4
 
5
5
  `ccprofile` lets you run **multiple Claude Code accounts in parallel** — one per terminal, one per project — with zero manual switching. It never touches Claude Code's own Keychain entry, so there is no global "active account" to corrupt.
6
6
 
7
+ ## 🤖 Install with an AI agent
8
+
9
+ Paste this prompt into Claude Code, Cursor, or any coding agent:
10
+
11
+ ```
12
+ Install and configure ccprofile by following the instructions here:
13
+ https://raw.githubusercontent.com/efoo-team/ccprofile/main/docs/install-for-agents.md
14
+ ```
15
+
16
+ The agent will check prerequisites (direnv, hooks), install the CLI, ask
17
+ whether you want shell completion, and walk you through registering accounts —
18
+ only the browser OAuth step needs your hands.
19
+
7
20
  ## Why
8
21
 
9
22
  Claude Code stores its OAuth credentials in a single macOS Keychain entry, shared across every `CLAUDE_CONFIG_DIR` profile ([#20553](https://github.com/anthropics/claude-code/issues/20553)). Switcher-style tools work around this by swapping that entry in place — which breaks down the moment two sessions with different accounts run at the same time (in-session token refresh writes the old account back).
@@ -11,8 +24,8 @@ Claude Code stores its OAuth credentials in a single macOS Keychain entry, share
11
24
  `ccprofile` takes the declarative route instead:
12
25
 
13
26
  - Each account's **long-lived OAuth token** (`claude setup-token`, valid ~1 year) is stored in the Keychain under ccprofile's own namespace — one entry per profile, no sharing, no swapping.
14
- - `ccprofile link` writes a **self-contained `.envrc`** that exports `CLAUDE_CODE_OAUTH_TOKEN` straight from the Keychain. direnv activates it when you enter the directory. No node/npx in the hot path.
15
- - `CLAUDE_CODE_OAUTH_TOKEN` outranks the stored login in Claude Code's [documented auth precedence](https://code.claude.com/docs/en/authentication#authentication-precedence), so linked directories route to their account and everywhere else falls back to your normal `/login`.
27
+ - `ccprofile link` writes a **self-contained `.envrc`** that exports the token as `ANTHROPIC_AUTH_TOKEN` straight from the Keychain. direnv activates it when you enter the directory. No node/npx in the hot path.
28
+ - `ANTHROPIC_AUTH_TOKEN` outranks the stored login in Claude Code's [documented auth precedence](https://code.claude.com/docs/en/authentication#authentication-precedence), so linked directories route to their account and everywhere else falls back to your normal `/login`.
16
29
 
17
30
  Auth state lives in each process's environment — parallel sessions cannot interfere with each other by construction.
18
31
 
@@ -45,7 +58,7 @@ ccprofile link work
45
58
  ccprofile link work ~/src/my-project
46
59
 
47
60
  # 3. Done — any claude launched in that directory (and below) runs as "work"
48
- claude # /status shows "Auth token: CLAUDE_CODE_OAUTH_TOKEN"
61
+ claude # /status shows "Auth token: ANTHROPIC_AUTH_TOKEN"
49
62
  ```
50
63
 
51
64
  Repeat with `ccprofile add personal` etc. Different terminals in different directories run different accounts concurrently.
@@ -60,7 +73,7 @@ Repeat with `ccprofile add personal` etc. Different terminals in different direc
60
73
  | `ccprofile unlink [dir]` | Remove the managed block (deletes `.envrc` if nothing else remains) |
61
74
  | `ccprofile token <name>` | Print the stored token to stdout (for scripting — handle with care) |
62
75
  | `ccprofile remove <name>` | Delete the profile and its Keychain entry |
63
- | `ccprofile doctor [dir]` | Diagnose overriding env vars (`ANTHROPIC_API_KEY` etc.), `apiKeyHelper`, expiry, token liveness, broken links. `--offline` skips the server probe |
76
+ | `ccprofile doctor [dir]` | Diagnose provider overrides, stale/missing active token env, expiry, token liveness, broken links. `--offline` skips the server probe |
64
77
  | `ccprofile completion <shell>` | Print a completion script for fish, zsh, or bash |
65
78
 
66
79
  ## Shell completion
@@ -89,7 +102,10 @@ macOS Keychain service "ccprofile", one entry per profile (the sec
89
102
 
90
103
  # >>> ccprofile managed >>>
91
104
  # profile: work
92
- export CLAUDE_CODE_OAUTH_TOKEN="$(security find-generic-password -w -s 'ccprofile' -a 'work' 2>/dev/null)"
105
+ _ccprofile_token="$(security find-generic-password -w -s 'ccprofile' -a 'work' 2>/dev/null)"
106
+ export ANTHROPIC_AUTH_TOKEN="$_ccprofile_token"
107
+ unset CLAUDE_CODE_OAUTH_TOKEN
108
+ unset _ccprofile_token
93
109
  # <<< ccprofile managed <<<
94
110
  ```
95
111
 
@@ -97,6 +113,7 @@ Notes:
97
113
 
98
114
  - Tokens are written to the Keychain via `security -i` (stdin), so secrets never appear in `ps` output.
99
115
  - The `.envrc` block is **self-contained**: direnv re-evaluates it on every directory entry, and it must stay fast and dependency-free. ccprofile is only needed for CRUD operations.
116
+ - Claude Code v2.1.199 treats `CLAUDE_CODE_OAUTH_TOKEN` as suitable for SDK/non-interactive automation, but interactive TUI sessions still consult `/login` account policy and quota. ccprofile therefore injects the same setup-token as `ANTHROPIC_AUTH_TOKEN`, which the interactive TUI uses as the active bearer token.
100
117
  - Add `.envrc` to your project's `.gitignore` — it is machine-local.
101
118
 
102
119
  ## Limitations — the price of parallel accounts
@@ -108,10 +125,8 @@ ccprofile is built on `claude setup-token`, whose long-lived tokens are **delibe
108
125
  - **`/status` → Usage tab shows no plan utilization** in token-authenticated sessions (same scope restriction). Check usage on claude.ai instead.
109
126
  - **Remote Control is unavailable** in token-authenticated sessions; it requires a full-scope login token.
110
127
  - **Tokens last up to 1 year but can die earlier** (password change, logout-all). The recorded expiry is a hint, not a guarantee — `ccprofile doctor` probes the server and tells live tokens apart from revoked ones.
111
- - **`claude --bare` does not read `CLAUDE_CODE_OAUTH_TOKEN`.**
112
- - **Subscription accounting:** from June 15, 2026, `claude -p` / Agent SDK usage on subscription plans draws from a separate monthly Agent SDK credit.
113
- - **direnv only sees shell-launched processes.** Apps started outside a hooked shell (GUI launchers) bypass the routing.
114
- - **Higher-precedence auth wins silently.** `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `apiKeyHelper`, and Bedrock/Vertex/Foundry env vars all outrank the token — `ccprofile doctor` flags them.
128
+ - **Routing only applies to shell-launched processes.** direnv activates the token when a hooked shell enters the directory; apps launched outside a hooked shell (GUI launchers) bypass it.
129
+ - **Cloud provider auth wins silently.** Bedrock/Vertex/Foundry env vars outrank `ANTHROPIC_AUTH_TOKEN`; `ccprofile doctor` flags them.
115
130
  - **macOS only** for now (the token store is the macOS Keychain).
116
131
 
117
132
  ## Development
@@ -123,6 +138,22 @@ pnpm test # vitest
123
138
  node dist/index.js --help
124
139
  ```
125
140
 
141
+ ## Release
142
+
143
+ Releases are automated with GitHub Actions + semantic-release.
144
+
145
+ - Merging to `main` runs CI and then `semantic-release`.
146
+ - Versioning is derived from Conventional Commits:
147
+ - `fix:` / `perf:` -> patch release
148
+ - `feat:` -> minor release
149
+ - `feat!:` or `BREAKING CHANGE:` -> major release
150
+ - `docs:` / `test:` / `ci:` / `chore:` -> no npm release
151
+ - Do not manually edit `package.json` version for normal releases; semantic-release updates the published package version.
152
+ - npm publishing uses trusted publishing (OIDC). Configure npm package `@efoo/ccprofile` with GitHub trusted publisher:
153
+ - repository: `efoo-team/ccprofile`
154
+ - workflow: `.github/workflows/release.yml`
155
+ - environment: none
156
+
126
157
  ## License
127
158
 
128
159
  MIT
@@ -68,6 +68,9 @@ export async function addCommand(argv) {
68
68
  saveConfig(config);
69
69
  console.log(ok(`Profile ${bold(name)} saved (Keychain: ${KEYCHAIN_SERVICE}/${name}).`));
70
70
  console.log(dim(`Token recorded as expiring at ${expiresAt} (setup-token issues 1-year tokens).`));
71
+ if (values.force) {
72
+ console.log(warn("Existing shells in linked directories may still export the old token. Run `direnv reload` there and restart Claude Code."));
73
+ }
71
74
  console.log(`\nNext: route a project directory to this account:\n ${cyan(`ccprofile link ${name} <project-dir>`)}`);
72
75
  return 0;
73
76
  }
@@ -9,16 +9,14 @@ import { Keychain } from "../lib/keychain.js";
9
9
  import { probeToken } from "../lib/probe.js";
10
10
  import { bold, fail, ok, warn } from "../lib/format.js";
11
11
  /**
12
- * Env vars that outrank CLAUDE_CODE_OAUTH_TOKEN in Claude Code's documented
13
- * authentication precedence. If any is set, ccprofile routing is silently
14
- * bypassed that is the failure mode this command exists to catch.
12
+ * Env vars that outrank ccprofile's managed ANTHROPIC_AUTH_TOKEN in Claude
13
+ * Code's documented authentication precedence. If any is set, ccprofile
14
+ * routing is bypassed before the token is considered.
15
15
  */
16
16
  const OVERRIDING_ENV_VARS = [
17
17
  "CLAUDE_CODE_USE_BEDROCK",
18
18
  "CLAUDE_CODE_USE_VERTEX",
19
19
  "CLAUDE_CODE_USE_FOUNDRY",
20
- "ANTHROPIC_AUTH_TOKEN",
21
- "ANTHROPIC_API_KEY",
22
20
  ];
23
21
  export async function doctorCommand(argv) {
24
22
  const { values, positionals } = parseArgs({
@@ -53,7 +51,7 @@ export async function doctorCommand(argv) {
53
51
  }
54
52
  for (const envVar of OVERRIDING_ENV_VARS) {
55
53
  if (process.env[envVar] !== undefined) {
56
- console.log(fail(`${envVar} is set: it overrides CLAUDE_CODE_OAUTH_TOKEN and bypasses ccprofile routing.`));
54
+ console.log(fail(`${envVar} is set: it overrides ANTHROPIC_AUTH_TOKEN and bypasses ccprofile routing.`));
57
55
  problems += 1;
58
56
  }
59
57
  }
@@ -63,8 +61,8 @@ export async function doctorCommand(argv) {
63
61
  try {
64
62
  const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
65
63
  if (settings.apiKeyHelper !== undefined) {
66
- console.log(fail(`apiKeyHelper is configured in ${settingsPath}: it overrides CLAUDE_CODE_OAUTH_TOKEN.`));
67
- problems += 1;
64
+ console.log(warn(`apiKeyHelper is configured in ${settingsPath}; linked ccprofile directories use ANTHROPIC_AUTH_TOKEN, which takes precedence.`));
65
+ warnings += 1;
68
66
  }
69
67
  }
70
68
  catch {
@@ -125,6 +123,24 @@ export async function doctorCommand(argv) {
125
123
  }
126
124
  else if (config.profiles[linked]) {
127
125
  console.log(ok(`${bold(dir)} is linked to profile "${linked}".`));
126
+ if (dir === resolve(process.cwd())) {
127
+ const linkedProfile = config.profiles[linked];
128
+ const exportedToken = process.env.ANTHROPIC_AUTH_TOKEN;
129
+ if (exportedToken === undefined) {
130
+ console.log(fail("Current shell does not export ANTHROPIC_AUTH_TOKEN. Run: direnv reload"));
131
+ problems += 1;
132
+ }
133
+ else {
134
+ const linkedToken = await keychain.getToken(linkedProfile.keychain.service, linkedProfile.keychain.account);
135
+ if (linkedToken !== null && exportedToken !== linkedToken) {
136
+ console.log(fail(`Current shell exports a different ANTHROPIC_AUTH_TOKEN than profile "${linked}". Run: direnv reload, then restart Claude Code.`));
137
+ problems += 1;
138
+ }
139
+ else if (linkedToken !== null) {
140
+ console.log(ok(`Current shell exports profile "${linked}" token via ANTHROPIC_AUTH_TOKEN.`));
141
+ }
142
+ }
143
+ }
128
144
  }
129
145
  else {
130
146
  console.log(fail(`${envrcPath} references unknown profile "${linked}". Run: ccprofile link <profile> ${dir}`));
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ ${bold("Commands")}
29
29
  ${bold("Typical flow")}
30
30
  ccprofile add work --email you@company.example
31
31
  ccprofile link work ~/src/my-project
32
- cd ~/src/my-project && claude # runs as "work" via CLAUDE_CODE_OAUTH_TOKEN
32
+ cd ~/src/my-project && claude # runs as "work" via ANTHROPIC_AUTH_TOKEN
33
33
  `;
34
34
  async function main() {
35
35
  const [command, ...rest] = process.argv.slice(2);
package/dist/lib/envrc.js CHANGED
@@ -12,7 +12,10 @@ export function renderBlock(profile, service, account) {
12
12
  return [
13
13
  BEGIN,
14
14
  `# profile: ${profile}`,
15
- `export CLAUDE_CODE_OAUTH_TOKEN="$(security find-generic-password -w -s '${service}' -a '${account}' 2>/dev/null)"`,
15
+ `_ccprofile_token="$(security find-generic-password -w -s '${service}' -a '${account}' 2>/dev/null)"`,
16
+ `export ANTHROPIC_AUTH_TOKEN="$_ccprofile_token"`,
17
+ `unset CLAUDE_CODE_OAUTH_TOKEN`,
18
+ `unset _ccprofile_token`,
16
19
  END,
17
20
  "",
18
21
  ].join("\n");
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@efoo/ccprofile",
3
- "version": "0.1.0",
4
- "description": "Per-directory Claude Code account routing via CLAUDE_CODE_OAUTH_TOKEN, direnv, and macOS Keychain",
3
+ "version": "0.1.2",
4
+ "description": "Per-directory Claude Code account routing via ANTHROPIC_AUTH_TOKEN, direnv, and macOS Keychain",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@10.33.4",
6
7
  "license": "MIT",
7
8
  "repository": {
8
9
  "type": "git",
@@ -38,10 +39,16 @@
38
39
  "typecheck": "tsc --noEmit",
39
40
  "test": "vitest run",
40
41
  "test:watch": "vitest",
42
+ "release": "semantic-release",
41
43
  "prepublishOnly": "pnpm run build"
42
44
  },
43
45
  "devDependencies": {
46
+ "@semantic-release/commit-analyzer": "^13.0.1",
47
+ "@semantic-release/github": "^12.0.9",
48
+ "@semantic-release/npm": "^13.1.5",
49
+ "@semantic-release/release-notes-generator": "^14.1.1",
44
50
  "@types/node": "^22.10.0",
51
+ "semantic-release": "^25.0.5",
45
52
  "typescript": "^5.7.0",
46
53
  "vitest": "^3.0.0"
47
54
  }