@efoo/ccprofile 0.1.1 → 0.1.3

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,6 +1,6 @@
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
 
@@ -24,8 +24,8 @@ Claude Code stores its OAuth credentials in a single macOS Keychain entry, share
24
24
  `ccprofile` takes the declarative route instead:
25
25
 
26
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.
27
- - `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.
28
- - `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`.
29
29
 
30
30
  Auth state lives in each process's environment — parallel sessions cannot interfere with each other by construction.
31
31
 
@@ -58,7 +58,7 @@ ccprofile link work
58
58
  ccprofile link work ~/src/my-project
59
59
 
60
60
  # 3. Done — any claude launched in that directory (and below) runs as "work"
61
- claude # /status shows "Auth token: CLAUDE_CODE_OAUTH_TOKEN"
61
+ claude # /status shows "Auth token: ANTHROPIC_AUTH_TOKEN"
62
62
  ```
63
63
 
64
64
  Repeat with `ccprofile add personal` etc. Different terminals in different directories run different accounts concurrently.
@@ -73,7 +73,7 @@ Repeat with `ccprofile add personal` etc. Different terminals in different direc
73
73
  | `ccprofile unlink [dir]` | Remove the managed block (deletes `.envrc` if nothing else remains) |
74
74
  | `ccprofile token <name>` | Print the stored token to stdout (for scripting — handle with care) |
75
75
  | `ccprofile remove <name>` | Delete the profile and its Keychain entry |
76
- | `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 |
77
77
  | `ccprofile completion <shell>` | Print a completion script for fish, zsh, or bash |
78
78
 
79
79
  ## Shell completion
@@ -102,7 +102,9 @@ macOS Keychain service "ccprofile", one entry per profile (the sec
102
102
 
103
103
  # >>> ccprofile managed >>>
104
104
  # profile: work
105
- 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 _ccprofile_token
106
108
  # <<< ccprofile managed <<<
107
109
  ```
108
110
 
@@ -119,12 +121,10 @@ ccprofile is built on `claude setup-token`, whose long-lived tokens are **delibe
119
121
  - **No account identity introspection.** The token cannot answer "whose token is this?" — the OAuth profile endpoint rejects it (`user:profile` scope missing, see [#11985](https://github.com/anthropics/claude-code/issues/11985)). The `--email` you record is a self-declared label, not verified.
120
122
  *Verify identity once, at registration time:* make sure the browser is logged into the intended claude.ai account before `claude setup-token`, then send a couple of prompts from a linked directory and confirm on claude.ai (web) that the intended account's usage moved.
121
123
  - **`/status` → Usage tab shows no plan utilization** in token-authenticated sessions (same scope restriction). Check usage on claude.ai instead.
122
- - **Remote Control is unavailable** in token-authenticated sessions; it requires a full-scope login token.
124
+ - **Remote Control is unavailable** in linked directories. Claude Code treats `ANTHROPIC_AUTH_TOKEN` sessions as API-key authentication, while Remote Control requires claude.ai subscription authentication.
123
125
  - **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.
124
- - **`claude --bare` does not read `CLAUDE_CODE_OAUTH_TOKEN`.**
125
- - **Subscription accounting:** from June 15, 2026, `claude -p` / Agent SDK usage on subscription plans draws from a separate monthly Agent SDK credit.
126
- - **direnv only sees shell-launched processes.** Apps started outside a hooked shell (GUI launchers) bypass the routing.
127
- - **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.
126
+ - **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.
127
+ - **Cloud provider auth wins silently.** Bedrock/Vertex/Foundry env vars outrank `ANTHROPIC_AUTH_TOKEN`; `ccprofile doctor` flags them.
128
128
  - **macOS only** for now (the token store is the macOS Keychain).
129
129
 
130
130
  ## Development
@@ -136,6 +136,22 @@ pnpm test # vitest
136
136
  node dist/index.js --help
137
137
  ```
138
138
 
139
+ ## Release
140
+
141
+ Releases are automated with GitHub Actions + semantic-release.
142
+
143
+ - Merging to `main` runs CI and then `semantic-release`.
144
+ - Versioning is derived from Conventional Commits:
145
+ - `fix:` / `perf:` -> patch release
146
+ - `feat:` -> minor release
147
+ - `feat!:` or `BREAKING CHANGE:` -> major release
148
+ - `docs:` / `test:` / `ci:` / `chore:` -> no npm release
149
+ - Do not manually edit `package.json` version for normal releases; semantic-release updates the published package version.
150
+ - npm publishing uses trusted publishing (OIDC). Configure npm package `@efoo/ccprofile` with GitHub trusted publisher:
151
+ - repository: `efoo-team/ccprofile`
152
+ - workflow: `.github/workflows/release.yml`
153
+ - environment: none
154
+
139
155
  ## License
140
156
 
141
157
  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,9 @@ 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 _ccprofile_token`,
16
18
  END,
17
19
  "",
18
20
  ].join("\n");
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@efoo/ccprofile",
3
- "version": "0.1.1",
4
- "description": "Per-directory Claude Code account routing via CLAUDE_CODE_OAUTH_TOKEN, direnv, and macOS Keychain",
3
+ "version": "0.1.3",
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
  }