@remeic/ccm 0.2.2 → 0.4.0
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 +47 -1
- package/dist/index.js +180 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,13 +13,30 @@
|
|
|
13
13
|
</table>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
+
## IMPORTANT: Claude Code ToS (Multi-Terminal Use)
|
|
17
|
+
|
|
18
|
+
> **Mandatory compliance rule for this project**
|
|
19
|
+
> Do **not** use the **same Claude account** from multiple terminals at the same time.
|
|
20
|
+
> We enforce: **1 account = 1 person = 1 active terminal session**.
|
|
21
|
+
|
|
22
|
+
- `ccm` isolates profiles. It does **not** grant any extra rights under Anthropic terms.
|
|
23
|
+
- For parallel work, use separate licensed accounts/seats (or API keys under Commercial Terms).
|
|
24
|
+
- No credential sharing, no shared account handoff, no "one account used by multiple people".
|
|
25
|
+
- This is an intentionally strict project rule to remove ambiguity and reduce compliance risk.
|
|
26
|
+
- The CLI also surfaces this via `ccm compliance` (alias: `ccm tos`) and after every successful `ccm create`.
|
|
27
|
+
|
|
28
|
+
Official basis (as of April 8, 2026):
|
|
29
|
+
- [Claude Code Legal & Compliance](https://code.claude.com/docs/en/legal-and-compliance): usage limits for Pro/Max assume ordinary, individual use; Anthropic may enforce auth restrictions without notice.
|
|
30
|
+
- [Anthropic Consumer Terms](https://www.anthropic.com/legal/consumer-terms): account credentials/API keys must not be shared or made available to others; Anthropic may suspend/terminate for material breach.
|
|
31
|
+
- [Anthropic Commercial Terms](https://www.anthropic.com/legal/commercial-terms): customer is responsible for all activity under its account.
|
|
32
|
+
|
|
16
33
|
[](https://github.com/remeic/ccm/actions/workflows/ci.yml)
|
|
17
34
|
[](https://www.npmjs.com/package/@remeic/ccm)
|
|
18
35
|
[](https://github.com/Remeic/homebrew-tap)
|
|
19
36
|
[](LICENSE)
|
|
20
37
|
[](https://nodejs.org)
|
|
21
38
|
[](https://codecov.io/github/Remeic/ccm)
|
|
22
|
-
[](https://dashboard.stryker-mutator.io/reports/github.com/Remeic/ccm/main)
|
|
23
40
|
|
|
24
41
|
Manage separate Claude Code profiles for personal, work, and client accounts without repeated logout/login cycles. `ccm` keeps each profile isolated so switching is immediate and parallel sessions stay clean.
|
|
25
42
|
|
|
@@ -30,11 +47,13 @@ Manage separate Claude Code profiles for personal, work, and client accounts wit
|
|
|
30
47
|
## Table of Contents
|
|
31
48
|
|
|
32
49
|
- [Table of Contents](#table-of-contents)
|
|
50
|
+
- [IMPORTANT: Claude Code ToS (Multi-Terminal Use)](#important-claude-code-tos-multi-terminal-use)
|
|
33
51
|
- [Why](#why)
|
|
34
52
|
- [Prerequisites](#prerequisites)
|
|
35
53
|
- [Install](#install)
|
|
36
54
|
- [Quick Start](#quick-start)
|
|
37
55
|
- [Commands](#commands)
|
|
56
|
+
- [Compliance Notice](#compliance-notice)
|
|
38
57
|
- [Passing Flags and Environment Variables](#passing-flags-and-environment-variables)
|
|
39
58
|
- [Multi-Account Login](#multi-account-login)
|
|
40
59
|
- [Different Browser per Profile](#different-browser-per-profile)
|
|
@@ -91,6 +110,11 @@ The installed command remains `ccm`.
|
|
|
91
110
|
```
|
|
92
111
|
$ ccm create work
|
|
93
112
|
✓ Profile "work" created
|
|
113
|
+
! Compliance notice
|
|
114
|
+
ccm isolates profiles but does not expand Anthropic usage rights.
|
|
115
|
+
Use each Claude account with one person and one active terminal session.
|
|
116
|
+
Do not share credentials or API keys across users or parallel operators.
|
|
117
|
+
Details: ccm compliance
|
|
94
118
|
Next: ccm login work
|
|
95
119
|
|
|
96
120
|
$ ccm login work
|
|
@@ -105,13 +129,32 @@ $ ccm use work
|
|
|
105
129
|
| Command | Description |
|
|
106
130
|
| -------------------------------------------------------- | ------------------------------------------------------------- |
|
|
107
131
|
| `ccm create <name> [-l label] [-b browser]` | Create a profile. `-b` sets the browser for OAuth |
|
|
132
|
+
| `ccm compliance` / `ccm tos` | Show the compliance notice, disclaimer, and official sources |
|
|
108
133
|
| `ccm list` | List all profiles with auth status, including drifted entries |
|
|
109
134
|
| `ccm use <name> [-- args]` | Launch Claude Code. Args after `--` are passed to Claude |
|
|
110
135
|
| `ccm login <name> [--console] [-b browser] [--url-only]` | Authenticate a profile |
|
|
111
136
|
| `ccm status [name]` | Show auth status and storage state for one or all profiles |
|
|
137
|
+
| `ccm rename <old-name> <new-name>` | Rename a profile (config, directory, and browser wrapper) |
|
|
112
138
|
| `ccm remove <name> [-f]` | Remove a profile. `-f` skips confirmation |
|
|
113
139
|
| `ccm run <name> -p <prompt>` | Run a prompt non-interactively |
|
|
114
140
|
|
|
141
|
+
## Compliance Notice
|
|
142
|
+
|
|
143
|
+
Every successful `ccm create` prints a short compliance warning. Users can re-open the full text at any time with:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
ccm compliance
|
|
147
|
+
ccm tos
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The notice is intentionally conservative:
|
|
151
|
+
|
|
152
|
+
- `ccm` is a profile-isolation tool, not a license-expansion tool.
|
|
153
|
+
- This project treats each Claude account as single-user and single-session.
|
|
154
|
+
- Shared credentials, shared operators, and parallel use of the same account are out of scope.
|
|
155
|
+
- If a team needs parallel access, they should use separate seats/accounts or API-key-based access under applicable Anthropic commercial terms.
|
|
156
|
+
- The notice is compliance guidance, not legal advice. Users remain responsible for reviewing Anthropic terms for their exact workflow.
|
|
157
|
+
|
|
115
158
|
## Passing Flags and Environment Variables
|
|
116
159
|
|
|
117
160
|
Everything after `--` in `ccm use` is forwarded to `claude`. Env vars from your shell are inherited.
|
|
@@ -198,13 +241,16 @@ src/
|
|
|
198
241
|
│ ├── profiles.ts # Profile directory management and validation
|
|
199
242
|
│ ├── profile-store.ts # Reconciled config/filesystem profile view
|
|
200
243
|
│ ├── claude.ts # Claude binary discovery, spawning, auth status
|
|
244
|
+
│ ├── compliance.ts # Centralized compliance notice text and official sources
|
|
201
245
|
│ └── browsers.ts # Browser wrapper generation and validation
|
|
202
246
|
└── commands/
|
|
247
|
+
├── compliance.ts # Dedicated compliance/TOS command
|
|
203
248
|
├── create.ts # Create profile (with rollback on failure)
|
|
204
249
|
├── list.ts # List profiles with auth status and drift state
|
|
205
250
|
├── login.ts # Authenticate via Claude TUI or --console
|
|
206
251
|
├── use.ts # Launch Claude with profile config dir
|
|
207
252
|
├── status.ts # Show auth status and drift state
|
|
253
|
+
├── rename.ts # Rename profile with atomic rollback
|
|
208
254
|
├── remove.ts # Remove profile with staged rollback
|
|
209
255
|
└── run.ts # Run prompt with specific profile
|
|
210
256
|
```
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,68 @@ import { z } from 'zod';
|
|
|
7
7
|
import { spawn, execFileSync } from 'child_process';
|
|
8
8
|
import { createInterface } from 'readline';
|
|
9
9
|
|
|
10
|
+
// src/lib/compliance.ts
|
|
11
|
+
var COMPLIANCE_CHECKED_AT = "2026-04-08";
|
|
12
|
+
var COMPLIANCE_SOURCES = [
|
|
13
|
+
{
|
|
14
|
+
label: "Claude Code Legal & Compliance",
|
|
15
|
+
url: "https://code.claude.com/docs/en/legal-and-compliance",
|
|
16
|
+
summary: "Pro and Max usage limits assume ordinary, individual use. Anthropic may enforce authentication restrictions without notice."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Anthropic Consumer Terms",
|
|
20
|
+
url: "https://www.anthropic.com/legal/consumer-terms",
|
|
21
|
+
summary: "Account login information, API keys, and account credentials must not be shared or made available to anyone else."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Anthropic Commercial Terms",
|
|
25
|
+
url: "https://www.anthropic.com/legal/commercial-terms",
|
|
26
|
+
summary: "Commercial customers are responsible for all activity and fees incurred under their account."
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
function buildRuleLines() {
|
|
30
|
+
return [
|
|
31
|
+
"Project compliance boundary:",
|
|
32
|
+
"- ccm isolates profiles, but it does not grant any extra rights under Anthropic terms.",
|
|
33
|
+
"- Treat each Claude account as single-user and single-session: 1 account = 1 person = 1 active terminal session.",
|
|
34
|
+
"- Do not share account credentials or API keys, and do not hand off the same account between multiple operators.",
|
|
35
|
+
"- For parallel operators, separate seats/accounts or API-key-based access under Commercial Terms are required."
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function getCompactComplianceNoticeLines() {
|
|
39
|
+
return [
|
|
40
|
+
"\x1B[33m!\x1B[0m Compliance notice",
|
|
41
|
+
" ccm isolates profiles but does not expand Anthropic usage rights.",
|
|
42
|
+
" Use each Claude account with one person and one active terminal session.",
|
|
43
|
+
" Do not share credentials or API keys across users or parallel operators.",
|
|
44
|
+
" Details: ccm compliance"
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function getFullComplianceNoticeLines() {
|
|
48
|
+
const sourceLines = COMPLIANCE_SOURCES.flatMap((source) => [
|
|
49
|
+
`- ${source.label}: ${source.url}`,
|
|
50
|
+
` ${source.summary}`
|
|
51
|
+
]);
|
|
52
|
+
return [
|
|
53
|
+
"Claude Code compliance notice",
|
|
54
|
+
"This project publishes an intentionally conservative operational rule to reduce misuse and ambiguity.",
|
|
55
|
+
...buildRuleLines(),
|
|
56
|
+
"Important:",
|
|
57
|
+
"- This notice is compliance guidance for ccm users. It is not legal advice and it does not replace review of Anthropic terms for your specific use case.",
|
|
58
|
+
"- If you are building a product, automation, or shared workflow around Claude access, verify your model with Anthropic before deployment.",
|
|
59
|
+
`Official sources reviewed on ${COMPLIANCE_CHECKED_AT}:`,
|
|
60
|
+
...sourceLines
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/commands/compliance.ts
|
|
65
|
+
function registerCompliance(program2) {
|
|
66
|
+
program2.command("compliance").alias("tos").description("Show the Claude Code compliance notice and official source links").action(() => {
|
|
67
|
+
for (const line of getFullComplianceNoticeLines()) {
|
|
68
|
+
console.log(line);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
10
72
|
var CCM_HOME = join(homedir(), ".ccm");
|
|
11
73
|
var PROFILES_DIR = join(CCM_HOME, "profiles");
|
|
12
74
|
var CONFIG_FILE = join(CCM_HOME, "config.json");
|
|
@@ -48,6 +110,14 @@ function getStagedPath(path) {
|
|
|
48
110
|
function getBrowserWrapperPath(profileName, browsersDir = BROWSERS_DIR) {
|
|
49
111
|
return join(browsersDir, `${profileName}.sh`);
|
|
50
112
|
}
|
|
113
|
+
function renameBrowserWrapper(oldName, newName, browsersDir = BROWSERS_DIR) {
|
|
114
|
+
const oldPath = getBrowserWrapperPath(oldName, browsersDir);
|
|
115
|
+
if (!existsSync(oldPath)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const newPath = getBrowserWrapperPath(newName, browsersDir);
|
|
119
|
+
renameSync(oldPath, newPath);
|
|
120
|
+
}
|
|
51
121
|
function removeBrowserWrapper(profileName, browsersDir = BROWSERS_DIR) {
|
|
52
122
|
const wrapperPath = getBrowserWrapperPath(profileName, browsersDir);
|
|
53
123
|
if (!existsSync(wrapperPath)) {
|
|
@@ -131,6 +201,18 @@ function removeProfile(name, configFile = CONFIG_FILE) {
|
|
|
131
201
|
delete config.profiles[name];
|
|
132
202
|
saveConfig(config, configFile);
|
|
133
203
|
}
|
|
204
|
+
function renameProfile(oldName, newName, configFile = CONFIG_FILE) {
|
|
205
|
+
const config = loadConfig(configFile);
|
|
206
|
+
if (!config.profiles[oldName]) {
|
|
207
|
+
throw new Error(`Profile "${oldName}" not found`);
|
|
208
|
+
}
|
|
209
|
+
if (config.profiles[newName]) {
|
|
210
|
+
throw new Error(`Profile "${newName}" already exists`);
|
|
211
|
+
}
|
|
212
|
+
config.profiles[newName] = { ...config.profiles[oldName], name: newName };
|
|
213
|
+
delete config.profiles[oldName];
|
|
214
|
+
saveConfig(config, configFile);
|
|
215
|
+
}
|
|
134
216
|
function getProfile(name, configFile = CONFIG_FILE) {
|
|
135
217
|
return loadConfig(configFile).profiles[name];
|
|
136
218
|
}
|
|
@@ -155,6 +237,17 @@ function createProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
|
155
237
|
mkdirSync(dir, { recursive: true });
|
|
156
238
|
return dir;
|
|
157
239
|
}
|
|
240
|
+
function renameProfileDir(oldName, newName, profilesDir = PROFILES_DIR) {
|
|
241
|
+
const oldDir = join(profilesDir, oldName);
|
|
242
|
+
const newDir = join(profilesDir, newName);
|
|
243
|
+
if (!existsSync(oldDir)) {
|
|
244
|
+
throw new Error(`Profile directory "${oldName}" does not exist`);
|
|
245
|
+
}
|
|
246
|
+
if (existsSync(newDir)) {
|
|
247
|
+
throw new Error(`Profile directory "${newName}" already exists`);
|
|
248
|
+
}
|
|
249
|
+
renameSync(oldDir, newDir);
|
|
250
|
+
}
|
|
158
251
|
function removeProfileDir(name, profilesDir = PROFILES_DIR) {
|
|
159
252
|
const dir = join(profilesDir, name);
|
|
160
253
|
if (!existsSync(dir)) {
|
|
@@ -225,6 +318,9 @@ function registerCreate(program2) {
|
|
|
225
318
|
throw configErr;
|
|
226
319
|
}
|
|
227
320
|
console.log(`\x1B[32m\u2713\x1B[0m Profile "${name}" created`);
|
|
321
|
+
for (const line of getCompactComplianceNoticeLines()) {
|
|
322
|
+
console.log(line);
|
|
323
|
+
}
|
|
228
324
|
console.log(` Next: ccm login ${name}`);
|
|
229
325
|
} catch (e) {
|
|
230
326
|
console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -288,8 +384,6 @@ function getAuthStatus(profileDir) {
|
|
|
288
384
|
});
|
|
289
385
|
});
|
|
290
386
|
}
|
|
291
|
-
|
|
292
|
-
// src/lib/profile-store.ts
|
|
293
387
|
function getProfileState(hasConfig, hasDirectory) {
|
|
294
388
|
if (hasConfig && hasDirectory) return "ready";
|
|
295
389
|
if (hasDirectory) return "orphaned";
|
|
@@ -383,6 +477,74 @@ function removeStoredProfile(name, configFile = CONFIG_FILE, profilesDir = PROFI
|
|
|
383
477
|
throw error;
|
|
384
478
|
}
|
|
385
479
|
}
|
|
480
|
+
function renameStoredProfile(oldName, newName, configFile = CONFIG_FILE, profilesDir = PROFILES_DIR, browsersDir = BROWSERS_DIR) {
|
|
481
|
+
validateProfileName(newName);
|
|
482
|
+
if (oldName === newName) {
|
|
483
|
+
throw new Error("Old and new profile names are the same");
|
|
484
|
+
}
|
|
485
|
+
const profile = getStoredProfile(oldName, configFile, profilesDir);
|
|
486
|
+
if (!profile) {
|
|
487
|
+
throw new Error(`Profile "${oldName}" does not exist`);
|
|
488
|
+
}
|
|
489
|
+
if (getProfile(newName, configFile)) {
|
|
490
|
+
throw new Error(`Profile "${newName}" already exists`);
|
|
491
|
+
}
|
|
492
|
+
if (profileExists(newName, profilesDir)) {
|
|
493
|
+
throw new Error(`Profile directory "${newName}" already exists`);
|
|
494
|
+
}
|
|
495
|
+
if (profile.hasDirectory) {
|
|
496
|
+
renameProfileDir(oldName, newName, profilesDir);
|
|
497
|
+
}
|
|
498
|
+
const hadWrapper = existsSync(getBrowserWrapperPath(oldName, browsersDir));
|
|
499
|
+
if (hadWrapper) {
|
|
500
|
+
try {
|
|
501
|
+
renameBrowserWrapper(oldName, newName, browsersDir);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
try {
|
|
504
|
+
rollbackRename(oldName, newName, profile.hasDirectory, false, profilesDir, browsersDir);
|
|
505
|
+
} catch (rollbackError) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError(error)}. Rollback failed: ${formatError(rollbackError)}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
renameProfile(oldName, newName, configFile);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
try {
|
|
517
|
+
rollbackRename(oldName, newName, profile.hasDirectory, hadWrapper, profilesDir, browsersDir);
|
|
518
|
+
} catch (rollbackError) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Failed to rename profile "${oldName}" to "${newName}": ${formatError(error)}. Rollback failed: ${formatError(rollbackError)}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function rollbackRename(oldName, newName, hadDirectory, hadWrapper, profilesDir, browsersDir) {
|
|
527
|
+
const rollbackErrors = [];
|
|
528
|
+
if (hadWrapper) {
|
|
529
|
+
try {
|
|
530
|
+
renameBrowserWrapper(newName, oldName, browsersDir);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
rollbackErrors.push(`browser wrapper: ${formatError(error)}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (hadDirectory) {
|
|
536
|
+
try {
|
|
537
|
+
renameProfileDir(newName, oldName, profilesDir);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
rollbackErrors.push(`profile dir: ${formatError(error)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (rollbackErrors.length > 0) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Rollback failed for rename "${oldName}" to "${newName}": ${rollbackErrors.join("; ")}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
386
548
|
function rollbackStorage(name, stagedDir, stagedWrapper, profilesDir, browsersDir) {
|
|
387
549
|
const rollbackErrors = [];
|
|
388
550
|
if (stagedDir) {
|
|
@@ -507,6 +669,19 @@ function registerRemove(program2) {
|
|
|
507
669
|
});
|
|
508
670
|
}
|
|
509
671
|
|
|
672
|
+
// src/commands/rename.ts
|
|
673
|
+
function registerRename(program2) {
|
|
674
|
+
program2.command("rename <old-name> <new-name>").description("Rename a profile").action((oldName, newName) => {
|
|
675
|
+
try {
|
|
676
|
+
renameStoredProfile(oldName, newName);
|
|
677
|
+
console.log(`\x1B[32m\u2713\x1B[0m Profile "${oldName}" renamed to "${newName}"`);
|
|
678
|
+
} catch (e) {
|
|
679
|
+
console.error(`\x1B[31m\u2717\x1B[0m ${e instanceof Error ? e.message : String(e)}`);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
510
685
|
// src/commands/run.ts
|
|
511
686
|
function registerRun(program2) {
|
|
512
687
|
program2.command("run <name>").description("Run Claude Code with a prompt using a profile").requiredOption("-p, --prompt <prompt>", "Prompt to send").action((name, opts) => {
|
|
@@ -591,10 +766,12 @@ function registerUse(program2) {
|
|
|
591
766
|
}
|
|
592
767
|
|
|
593
768
|
// src/index.ts
|
|
594
|
-
var program = new Command().name("ccm").version("0.
|
|
769
|
+
var program = new Command().name("ccm").version("0.4.0").description("Manage multiple Claude Code profiles");
|
|
595
770
|
registerCreate(program);
|
|
771
|
+
registerCompliance(program);
|
|
596
772
|
registerList(program);
|
|
597
773
|
registerRemove(program);
|
|
774
|
+
registerRename(program);
|
|
598
775
|
registerLogin(program);
|
|
599
776
|
registerStatus(program);
|
|
600
777
|
registerUse(program);
|