@efoo/ccprofile 0.1.1 → 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 +28 -10
- 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 +4 -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,10 @@ 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 CLAUDE_CODE_OAUTH_TOKEN
|
|
108
|
+
unset _ccprofile_token
|
|
106
109
|
# <<< ccprofile managed <<<
|
|
107
110
|
```
|
|
108
111
|
|
|
@@ -110,6 +113,7 @@ Notes:
|
|
|
110
113
|
|
|
111
114
|
- Tokens are written to the Keychain via `security -i` (stdin), so secrets never appear in `ps` output.
|
|
112
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.
|
|
113
117
|
- Add `.envrc` to your project's `.gitignore` — it is machine-local.
|
|
114
118
|
|
|
115
119
|
## Limitations — the price of parallel accounts
|
|
@@ -121,10 +125,8 @@ ccprofile is built on `claude setup-token`, whose long-lived tokens are **delibe
|
|
|
121
125
|
- **`/status` → Usage tab shows no plan utilization** in token-authenticated sessions (same scope restriction). Check usage on claude.ai instead.
|
|
122
126
|
- **Remote Control is unavailable** in token-authenticated sessions; it requires a full-scope login token.
|
|
123
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.
|
|
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.
|
|
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.
|
|
128
130
|
- **macOS only** for now (the token store is the macOS Keychain).
|
|
129
131
|
|
|
130
132
|
## Development
|
|
@@ -136,6 +138,22 @@ pnpm test # vitest
|
|
|
136
138
|
node dist/index.js --help
|
|
137
139
|
```
|
|
138
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
|
+
|
|
139
157
|
## License
|
|
140
158
|
|
|
141
159
|
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,10 @@ 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 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.
|
|
4
|
-
"description": "Per-directory Claude Code account routing via
|
|
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
|
}
|