@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 +27 -11
- package/dist/commands/add.js +3 -0
- package/dist/commands/doctor.js +24 -8
- package/dist/index.js +1 -1
- package/dist/lib/envrc.js +3 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ccprofile
|
|
2
2
|
|
|
3
|
-
Per-directory Claude Code account routing via `
|
|
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 `
|
|
28
|
-
- `
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
125
|
-
- **
|
|
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
|
package/dist/commands/add.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
13
|
-
* authentication precedence. If any is set, ccprofile
|
|
14
|
-
*
|
|
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
|
|
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(
|
|
67
|
-
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
4
|
-
"description": "Per-directory Claude Code account routing via
|
|
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
|
}
|