@ngcodes/ccpm 0.4.12 → 0.5.1

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.
Files changed (4) hide show
  1. package/README.md +82 -82
  2. package/bin/ccpm +5 -4
  3. package/install.js +89 -19
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -6,13 +6,11 @@
6
6
  [![npm](https://img.shields.io/npm/v/@ngcodes/ccpm)](https://www.npmjs.com/package/@ngcodes/ccpm)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
 
9
- ccpm (Claude Code Profile Manager) lets you create isolated profiles for Claude Code, each with its own credentials, settings, MCP servers, and memory. Open two terminals, run two different accounts at the same time.
9
+ ccpm (Claude Code Profile Manager) keeps multiple Claude Code profiles isolated on one machine. Each profile has its own credentials, settings, MCP servers, plugins, skills, and memory. Open two terminals, run two accounts.
10
10
 
11
11
  ## Why
12
12
 
13
- Claude Code reads config from a single directory (`~/.claude`). If you have a personal account and a work account, you cannot use both at the same time. Switching means logging out and back in, or manually swapping config files.
14
-
15
- ccpm fixes this. Each profile gets its own config directory. When you run `ccpm run <profile>`, it sets `CLAUDE_CONFIG_DIR` to the right directory and launches Claude Code. Two terminals, two profiles, zero conflicts.
13
+ Claude Code reads its config from a single directory (`~/.claude`), so without ccpm you can only be signed into one account at a time. ccpm gives every profile its own config directory and sets `CLAUDE_CONFIG_DIR` when launching claude. Two terminals, two profiles, zero conflicts.
16
14
 
17
15
  ## Install
18
16
 
@@ -30,19 +28,19 @@ go install github.com/nitin-1926/claude-code-profile-manager/ccpm@latest
30
28
  ## Quick start
31
29
 
32
30
  ```bash
33
- # Create profiles
34
- ccpm add personal # authenticate via OAuth or API key
35
- ccpm add work # same, with a different account
31
+ # Create profiles (each prompts for OAuth or API key)
32
+ ccpm add personal
33
+ ccpm add work
36
34
 
37
- # Run them in parallel
38
- ccpm run personal # in terminal 1
39
- ccpm run work # in terminal 2
35
+ # Run in parallel
36
+ ccpm run personal # terminal 1
37
+ ccpm run work # terminal 2
40
38
 
41
39
  # Check status
42
40
  ccpm list
43
41
  ```
44
42
 
45
- Output:
43
+ Sample output:
46
44
 
47
45
  ```
48
46
  NAME AUTH STATUS
@@ -50,113 +48,115 @@ personal oauth ✓ nitin@gmail.com
50
48
  work api_key ✓ sk-ant-...7f2k ★
51
49
  ```
52
50
 
51
+ ## Changelog
52
+
53
+ Release highlights and fixes: **[ccpm.dev/changelog](https://ccpm.dev/changelog)**.
54
+
53
55
  ## Key features
54
56
 
55
- - **Parallel sessions**: run different Claude Code accounts in different terminals simultaneously
56
- - **Full isolation**: each profile has its own credentials, settings, MCP servers, projects, and memory
57
- - **OAuth + API key**: supports both authentication methods per profile
58
- - **Asset management**: skills, agents, commands, rules, hooks install globally or per-profile; MCP servers install globally, per-profile, or per-project (`ccpm mcp add --scope global|profile|project`)
59
- - **MCP transports**: stdio, HTTP, and SSE. `ccpm mcp auth <server> --profile <p>` runs native claude's OAuth flow scoped to the profile.
60
- - **Plugin activation per profile**: override `enabledPlugins` so a plugin can be on in one profile and off in another
61
- - **Permissions UI**: `ccpm permissions allow|ask|deny|mode` writes `permissions.{allow,ask,deny,defaultMode}` directly no JSON surgery needed
62
- - **Per-profile env vars**: `ccpm env set` persists them; `ccpm run --ccpm-env KEY=VAL` overlays one-shot overrides. Unknown flags (`--dangerously-skip-permissions`, `--model`, ...) flow through to claude with no `--` separator needed.
63
- - **Sessions**: `ccpm sessions list <profile>` browses Claude Code's session files
64
- - **Settings**: native-Claude-compatible merge — `~/.claude/settings.json` → profile fragment → project `.claude/settings.{json,local.json}` → enterprise managed-settings (org policy, highest precedence)
65
- - **Encrypted vault**: AES-256-GCM encrypted credential backups with master key in your OS keychain
66
- - **IDE support**: set the default profile for VS Code with `ccpm set-default`
67
- - **Shell integration**: `ccpm use` sets the profile for your entire shell session
68
- - **Cross-platform**: macOS Keychain, Linux Secret Service, Windows Credential Manager
57
+ - **Parallel sessions**: run different Claude Code accounts in different terminals at the same time.
58
+ - **OAuth + API key** per profile, with credentials in the OS keychain.
59
+ - **Per-profile assets**: skills, agents, commands, rules, hooks, plugins, MCP servers, and settings install globally, per-profile, or per-project.
60
+ - **MCP transports**: stdio, HTTP, SSE. `ccpm mcp auth <server> --profile <p>` completes OAuth in the right profile.
61
+ - **Permissions and hooks** without hand-editing JSON.
62
+ - **IDE default**: `ccpm set-default` pins a profile for direct `claude` launches (VS Code, Cursor, Antigravity). On macOS this is set system-wide via a LaunchAgent.
63
+ - **Encrypted vault**: AES-256-GCM credential backups with a master key in your OS keychain.
69
64
 
70
65
  ## Commands
71
66
 
72
- | Command | Description |
73
- | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
74
- | `ccpm add <name>` | Create a new profile (OAuth or API key) with an interactive import wizard |
75
- | `ccpm run <name> [claude-args...]` | Launch Claude Code with a profile (unknown flags forward to claude) |
76
- | `ccpm use <name>` | Set profile for the current shell session |
77
- | `ccpm list` | List all profiles and their status |
78
- | `ccpm status` | Show system overview |
79
- | `ccpm doctor` | Diagnose env, auth health, root-vs-profile drift, symlink integrity |
80
- | `ccpm set-default <name>` | Set the default profile for IDEs |
81
- | `ccpm remove <name>` | Delete a profile |
82
- | `ccpm sync` | Sync global installs into profiles |
83
- | `ccpm import default` / `ccpm import from-profile` | Import assets from `~/.claude` or clone from another ccpm profile |
84
- | `ccpm skill/agent/command/rule add/remove/list/link` | Manage Claude Code asset types |
85
- | `ccpm plugin list/enable/disable` | Manage plugin activation per profile |
86
- | `ccpm hooks add/remove/list` | Manage hook entries in profile settings |
87
- | `ccpm mcp add/remove/list/import/auth` | Manage MCP servers (stdio, http, sse) scope global, profile, or project |
88
- | `ccpm env set/unset/list` | Persist env vars per profile (injected at `ccpm run`) |
89
- | `ccpm permissions allow/ask/deny/remove/list/mode` | Manage `permissions.*` rules and defaultMode |
90
- | `ccpm sessions list <profile>` | List Claude Code sessions stored in a profile |
91
- | `ccpm settings set/get/apply/show/statusline/outputstyle` | Manage Claude Code settings |
92
- | `ccpm auth status/refresh/backup/restore` | Manage authentication |
67
+ | Command | Description |
68
+ | --------------------------------------------------------- | -------------------------------------------------------------------- |
69
+ | `ccpm add <name>` | Create a new profile (OAuth or API key) via an interactive wizard |
70
+ | `ccpm run <name> [args...]` | Launch claude with the profile (unknown flags forward to claude) |
71
+ | `ccpm use <name>` | Set the profile for the current shell session |
72
+ | `ccpm list` / `ls` | List profiles with auth status (`--json` for machine-readable output) |
73
+ | `ccpm status [name]` | System overview, or auth health for one profile |
74
+ | `ccpm rename <old> <new>` | Rename a profile (migrates keychain entries and plugin paths) |
75
+ | `ccpm remove <name>` / `rm` | Delete a profile (`--force` to skip confirm) |
76
+ | `ccpm clone <src> <new>` | Duplicate a profile (assets + settings + auth; `--no-auth` available) |
77
+ | `ccpm set-default [name]` / `ccpm unset-default` | Pin or clear the default profile for direct `claude` launches |
78
+ | `ccpm prompt` | Print the active profile name for a shell prompt (PS1 / starship) |
79
+ | `ccpm sync` | Re-apply global installs into one or all profiles |
80
+ | `ccpm doctor` | Health check: env, auth, drift, symlinks, cascade (`--fix` prunes symlinks) |
81
+ | `ccpm export / import-bundle` | Export a profile to a portable `.tar.gz` and restore it elsewhere |
82
+ | `ccpm consolidate` | Audit (and optionally `--fix`) asset drift across scopes |
83
+ | `ccpm default fingerprint update / check` | Record or diff the `~/.claude` drift baseline |
84
+ | `ccpm trust add / remove / list` | Grant/revoke trust for project `.claude/` hooks, permissions, MCP |
85
+ | `ccpm import default / from-profile` | Import assets from `~/.claude` or clone from another ccpm profile |
86
+ | `ccpm skill / agent / command / rule add|remove|list|link`| Manage per-profile or global Claude Code asset trees |
87
+ | `ccpm plugin list / enable / disable / install / remove` | Marketplaces + per-profile activation; `gc` cleans the shared cache |
88
+ | `ccpm hooks add / remove / list` | Manage hook entries in profile settings |
89
+ | `ccpm mcp add / remove / list / import / auth` | Manage MCP servers (stdio, http, sse) at global/profile/project scope |
90
+ | `ccpm env set / unset / list` | Persist env vars per profile (injected at `ccpm run`) |
91
+ | `ccpm permissions allow / ask / deny / remove / list / mode` | Manage `permissions.*` rules and defaultMode |
92
+ | `ccpm sessions list <profile>` | List Claude Code sessions stored in a profile |
93
+ | `ccpm settings set / get / apply / show / statusline / outputstyle` | Manage Claude Code settings per profile |
94
+ | `ccpm auth status / refresh / backup / restore` | Manage authentication and the encrypted vault |
95
+ | `ccpm config set / get` | `cascade_auto_adopt`, `check_default_drift`, `default_dir` (get-only) |
96
+ | `ccpm shell-init` | Print the shell hook (zsh / bash / fish / powershell) |
97
+ | `ccpm completion <shell>` | Generate a shell completion script (bash / zsh / fish / powershell) |
98
+ | `ccpm uninstall` | Remove all profiles, keychain entries, vault, and `~/.ccpm/` |
99
+
100
+ Full command reference and guides: **[ccpm.dev/docs](https://ccpm.dev/docs)**.
101
+
102
+ `ccpm run` intercepts four flags before forwarding to claude: `--ccpm-env KEY=VAL` (one-shot env override, repeatable), `--no-auto-adopt` (skip the host-asset cascade scan), `--help`, `--version`. Use `--` to forward `--help` or `--version` to claude.
93
103
 
94
104
  ## How it works
95
105
 
96
- ccpm uses one official mechanism: the `CLAUDE_CONFIG_DIR` environment variable.
106
+ 1. `ccpm add` creates `~/.ccpm/profiles/<name>/` with its own config and credentials.
107
+ 2. `ccpm run` merges shared settings/MCP fragments, sets `CLAUDE_CONFIG_DIR`, and execs `claude`.
108
+ 3. Each terminal gets a completely isolated Claude Code instance.
97
109
 
98
- 1. `ccpm add` creates `~/.ccpm/profiles/<name>/` with its own config and credentials
99
- 2. `ccpm run` merges shared settings/MCP fragments, sets `CLAUDE_CONFIG_DIR`, and execs `claude`
100
- 3. Each terminal gets a completely isolated Claude Code instance
101
-
102
- Skills and MCP servers can be installed globally (`--global`, stored in `~/.ccpm/share/`) or per-profile (`--profile <name>`). For settings, the cross-profile baseline is the native `~/.claude/settings.json` file (ccpm merges it into every profile at launch); use `ccpm settings set --profile <name>` for per-profile overrides. Per-repo overrides live in `./.claude/settings.json` and are honored automatically.
110
+ Assets install globally (`--global`, stored in `~/.ccpm/share/`) or per-profile (`--profile <name>`). For settings, the cross-profile baseline is the native `~/.claude/settings.json`; ccpm merges it into every profile at launch. Per-repo overrides in `./.claude/settings.json` are honored automatically.
103
111
 
104
112
  No daemons. No patches. No magic.
105
113
 
106
114
  ## Privacy and security
107
115
 
108
- ccpm is 100% local. It never makes network requests, never collects data, and never phones home.
116
+ ccpm is 100% local. It never makes network requests, collects data, or phones home.
109
117
 
110
- - API keys are stored in your OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager)
111
- - Vault backups use AES-256-GCM encryption with a master key in your OS keychain
112
- - All data lives in `~/.ccpm/` on your machine
113
- - No telemetry, analytics, or tracking
118
+ - API keys live in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager).
119
+ - Vault backups use AES-256-GCM with a master key in your OS keychain.
120
+ - All data lives in `~/.ccpm/`. No telemetry, no analytics, no tracking.
114
121
 
115
122
  ## Platform support
116
123
 
117
- | Feature | macOS | Linux | Windows |
118
- | ------------------ | ---------------------------------------- | ----------------- | --------------------------------- |
119
- | OAuth per-profile | Keychain entry namespaced by profile dir | .credentials.json | .credentials.json |
120
- | API key storage | Keychain | Secret Service | Credential Manager |
121
- | Parallel sessions | Yes | Yes | Yes |
122
- | Shared skill dedup | Symlinks | Symlinks | Symlinks (Developer Mode) or copy |
123
- | Shell hook | zsh, bash, fish | zsh, bash, fish | PowerShell |
124
+ | Feature | macOS ✓ verified | Windows ⚠ experimental | Linux ⚠ experimental |
125
+ | ------------------ | ---------------------------------------- | ----------------------------------------- | -------------------- |
126
+ | OAuth per-profile | Keychain entry namespaced by profile dir | wincred entry, same namespacing model | `.credentials.json` |
127
+ | API key storage | Keychain | Credential Manager | Secret Service |
128
+ | Parallel sessions | Yes | Yes | Yes |
129
+ | Shared asset dedup | Symlinks | Symlinks (Developer Mode) or copy | Symlinks |
130
+ | Shell hook | zsh, bash, fish | PowerShell | zsh, bash, fish |
131
+ | `set-default` (system-wide GUI) | LaunchAgent + `launchctl setenv` | Keychain / identity only | Keychain / identity only |
124
132
 
125
- > **Requires Claude Code `v2.1.56` or newer for macOS OAuth isolation.** Earlier versions share a single keychain entry across all profiles. `ccpm doctor` warns on older versions.
133
+ > Requires **Claude Code v2.1.56+** on macOS for OAuth isolation. `ccpm doctor` warns when your Claude Code is too old.
126
134
 
127
135
  ## MCP authentication model
128
136
 
129
- MCP servers authenticate in one of three ways, and ccpm isolates each differently:
130
-
131
- 1. **Env-var-based (isolated)** tokens live in the per-profile MCP fragment. Each profile can carry a different value. Use `ccpm mcp add <name> --env KEY=VALUE --profile <name>`.
132
- 2. **OAuth MCPs (isolated)** — Claude Code caches OAuth tokens inside `<CLAUDE_CONFIG_DIR>/.claude.json`, which is per-profile.
133
- 3. **Globally-cached MCPs (shared)** — MCPs that write to `~/.config/<service>` or a fixed-name keychain entry are shared across every profile. ccpm cannot isolate them without upstream changes.
137
+ 1. **Env-var MCPs** (e.g. `GITHUB_TOKEN`): isolated per profile. Configure with `ccpm mcp add <name> --env KEY=VALUE --profile <p>`.
138
+ 2. **OAuth MCPs**: isolated, because tokens cache inside `<CLAUDE_CONFIG_DIR>/.claude.json` which is per-profile.
139
+ 3. **Globally-cached MCPs** (servers that write to `~/.config/<service>` or a non-namespaced OS keychain entry): **shared** across profiles. ccpm cannot isolate them.
134
140
 
135
141
  ## Known limitations
136
142
 
137
- - **VS Code extension**: The Claude VS Code extension always reads from `~/.claude`. Use `ccpm set-default` to point it at a ccpm profile. On macOS this copies the namespaced keychain entry into the default slot.
138
- - **Windows without Developer Mode**: ccpm falls back to copying shared assets instead of symlinking, and writes a marker at `~/.ccpm/.windows-copy-fallback`. Turn on Developer Mode for true deduplication.
139
- - **Globally-cached MCP servers** (see the MCP auth model above) cannot be isolated per profile.
140
- - **Linux headless**: `go-keyring` requires D-Bus and a secret service (gnome-keyring or kwallet). On headless servers, API key profiles need a running secret service.
143
+ - **IDE extensions ignore `CLAUDE_CONFIG_DIR`**: VS Code, Cursor, and Antigravity launch claude directly. Use `ccpm set-default` to pin a profile for them. macOS is system-wide; Linux and Windows keep keychain and identity sync but no LaunchAgent equivalent yet.
144
+ - **Windows without Developer Mode**: ccpm falls back to copying shared assets instead of symlinking and writes a marker at `~/.ccpm/.windows-copy-fallback`.
145
+ - **Globally-cached MCP servers** cannot be isolated per profile (see the MCP auth model).
146
+ - **Headless Linux**: `go-keyring` requires D-Bus and a secret service. API-key profiles need one running.
141
147
 
142
148
  ## Build from source
143
149
 
144
150
  ```bash
145
151
  git clone https://github.com/nitin-1926/claude-code-profile-manager.git
146
- cd claude-code-profile-manager/cli
152
+ cd claude-code-profile-manager/ccpm
147
153
  go build -o ccpm .
148
154
  ./ccpm --version
149
155
  ```
150
156
 
151
157
  ## Contributing
152
158
 
153
- Contributions are welcome. Please open an issue first to discuss what you want to change.
154
-
155
- 1. Fork the repo
156
- 2. Create a feature branch (`git checkout -b feature/your-feature`)
157
- 3. Make your changes
158
- 4. Run tests (`cd ccpm && go test ./...`)
159
- 5. Open a pull request
159
+ Open an issue first to discuss what you want to change, then fork, branch, test (`cd ccpm && go test ./...`), and open a PR.
160
160
 
161
161
  ## License
162
162
 
package/bin/ccpm CHANGED
@@ -10,11 +10,12 @@ const binary = path.join(__dirname, isWin ? "ccpm-native.exe" : "ccpm-native");
10
10
  if (!fs.existsSync(binary)) {
11
11
  console.error("ccpm: native binary not found at " + binary);
12
12
  console.error(
13
- "The postinstall step may have failed. Reinstall with `npm i -g @ngcodes/ccpm`"
14
- );
15
- console.error(
16
- "Or download manually: https://github.com/nitin-1926/claude-code-profile-manager/releases"
13
+ "The postinstall download was skipped (e.g. --ignore-scripts) or failed (e.g. offline). Fix it with any of:"
17
14
  );
15
+ console.error(" • Reinstall WITHOUT --ignore-scripts: npm i -g @ngcodes/ccpm");
16
+ console.error(" • Re-run the download in-place: node \"" + path.join(__dirname, "..", "install.js") + "\"");
17
+ console.error(" • Install via curl: curl -fsSL https://raw.githubusercontent.com/nitin-1926/claude-code-profile-manager/main/scripts/install.sh | sh");
18
+ console.error(" • Or download manually: https://github.com/nitin-1926/claude-code-profile-manager/releases");
18
19
  process.exit(1);
19
20
  }
20
21
 
package/install.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Downloads the correct ccpm binary for the current platform during npm install
3
3
 
4
4
  const { execSync } = require("child_process");
5
+ const crypto = require("crypto");
5
6
  const fs = require("fs");
6
7
  const path = require("path");
7
8
  const https = require("https");
@@ -9,6 +10,12 @@ const https = require("https");
9
10
  const REPO = "nitin-1926/claude-code-profile-manager";
10
11
  const BINARY = "ccpm";
11
12
 
13
+ // Pin the download to the version of this exact npm package. Querying
14
+ // releases/latest would let a freshly cut release ship a binary that doesn't
15
+ // match the installed package version (and could be raced by an attacker who
16
+ // can publish a release). require()ing package.json keeps the two in lockstep.
17
+ const VERSION = require("./package.json").version;
18
+
12
19
  function getPlatform() {
13
20
  const platform = process.platform;
14
21
  const arch = process.arch;
@@ -27,27 +34,45 @@ function getPlatform() {
27
34
  return { os, arch: cpu };
28
35
  }
29
36
 
30
- function getLatestVersion() {
37
+ // Fetch a small text resource (the checksums manifest), following redirects.
38
+ function fetchText(url) {
31
39
  return new Promise((resolve, reject) => {
32
- const url = `https://api.github.com/repos/${REPO}/releases/latest`;
33
- https.get(url, { headers: { "User-Agent": "ccpm-npm" } }, (res) => {
34
- let data = "";
35
- res.on("data", (chunk) => (data += chunk));
36
- res.on("end", () => {
37
- try {
38
- const json = JSON.parse(data);
39
- resolve(json.tag_name.replace(/^v/, ""));
40
- } catch {
41
- // Fallback to package.json version
42
- const pkg = require("./package.json");
43
- resolve(pkg.version);
40
+ const follow = (url) => {
41
+ https.get(url, { headers: { "User-Agent": "ccpm-npm" } }, (res) => {
42
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
43
+ follow(res.headers.location);
44
+ return;
44
45
  }
45
- });
46
- res.on("error", reject);
47
- });
46
+ if (res.statusCode !== 200) {
47
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
48
+ return;
49
+ }
50
+ let data = "";
51
+ res.on("data", (chunk) => (data += chunk));
52
+ res.on("end", () => resolve(data));
53
+ res.on("error", reject);
54
+ }).on("error", reject);
55
+ };
56
+ follow(url);
48
57
  });
49
58
  }
50
59
 
60
+ function sha256OfFile(filePath) {
61
+ const hash = crypto.createHash("sha256");
62
+ hash.update(fs.readFileSync(filePath));
63
+ return hash.digest("hex");
64
+ }
65
+
66
+ // Parse goreleaser's checksums.txt ("<sha256> <filename>" per line) and return
67
+ // the expected hash for archiveName, or null if it isn't listed.
68
+ function expectedChecksum(manifest, archiveName) {
69
+ for (const line of manifest.split("\n")) {
70
+ const m = line.match(/^([0-9a-fA-F]{64})\s+(\S+)\s*$/);
71
+ if (m && m[2] === archiveName) return m[1].toLowerCase();
72
+ }
73
+ return null;
74
+ }
75
+
51
76
  async function download(url, dest) {
52
77
  return new Promise((resolve, reject) => {
53
78
  const follow = (url) => {
@@ -85,10 +110,13 @@ function warnIfUnverifiedPlatform(os) {
85
110
  async function main() {
86
111
  const { os, arch } = getPlatform();
87
112
  warnIfUnverifiedPlatform(os);
88
- const version = await getLatestVersion();
113
+ const version = VERSION;
89
114
 
90
115
  const ext = os === "windows" ? "zip" : "tar.gz";
91
- const url = `https://github.com/${REPO}/releases/download/v${version}/${BINARY}_${os}_${arch}.${ext}`;
116
+ const archiveName = `${BINARY}_${os}_${arch}.${ext}`;
117
+ const baseUrl = `https://github.com/${REPO}/releases/download/v${version}`;
118
+ const url = `${baseUrl}/${archiveName}`;
119
+ const checksumsUrl = `${baseUrl}/checksums.txt`;
92
120
 
93
121
  console.log(`Installing ccpm v${version} for ${os}/${arch}...`);
94
122
 
@@ -102,7 +130,49 @@ async function main() {
102
130
  fs.mkdirSync(tmpDir, { recursive: true });
103
131
 
104
132
  const archivePath = path.join(tmpDir, `archive.${ext}`);
105
- await download(url, archivePath);
133
+ try {
134
+ await download(url, archivePath);
135
+ } catch (err) {
136
+ throw new Error(
137
+ `failed to download ${url}: ${err.message}\n` +
138
+ ` Is there a published release for v${version}? See https://github.com/${REPO}/releases`,
139
+ );
140
+ }
141
+
142
+ // Verify the archive against the SHA-256 published in the SAME release's
143
+ // checksums.txt. Fail closed: a missing manifest, a missing line, or a
144
+ // mismatch deletes the partial download and aborts — we never install an
145
+ // unverified binary that would run with the user's privileges.
146
+ let manifest;
147
+ try {
148
+ manifest = await fetchText(checksumsUrl);
149
+ } catch (err) {
150
+ fs.rmSync(tmpDir, { recursive: true, force: true });
151
+ throw new Error(
152
+ `failed to download ${checksumsUrl}: ${err.message}\n` +
153
+ ` Refusing to install without a checksum to verify against.`,
154
+ );
155
+ }
156
+
157
+ console.log("Verifying SHA-256...");
158
+ const expected = expectedChecksum(manifest, archiveName);
159
+ if (!expected) {
160
+ fs.rmSync(tmpDir, { recursive: true, force: true });
161
+ throw new Error(
162
+ `${archiveName} not listed in checksums.txt — possible tampering or missing release asset.`,
163
+ );
164
+ }
165
+ const actual = sha256OfFile(archivePath);
166
+ if (expected !== actual) {
167
+ fs.rmSync(tmpDir, { recursive: true, force: true });
168
+ throw new Error(
169
+ `checksum mismatch for ${archiveName}.\n` +
170
+ ` expected: ${expected}\n` +
171
+ ` actual: ${actual}\n` +
172
+ ` Refusing to install a tampered binary.`,
173
+ );
174
+ }
175
+ console.log(` sha256 ok (${expected})`);
106
176
 
107
177
  if (ext === "zip") {
108
178
  execSync(`unzip -o -q "${archivePath}" -d "${tmpDir}"`, { stdio: "inherit" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngcodes/ccpm",
3
- "version": "0.4.12",
3
+ "version": "0.5.1",
4
4
  "description": "Claude Code Profile Manager — manage multiple Claude Code accounts with isolated profiles",
5
5
  "bin": {
6
6
  "ccpm": "bin/ccpm"