any-buddy 1.0.7 → 1.0.9

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,4 +1,6 @@
1
- # claude-code-any-buddy
1
+ # Claude Code Any Buddy
2
+ ⚠️ Not tested on Windows, if claude session does not start after using this tool use the restore command or you may need to reinstall claude code. I have tested this on linux and mac and seems to work well
3
+
2
4
  Pick any Claude Code companion pet you want.
3
5
 
4
6
  ```bash
@@ -21,7 +23,7 @@ Claude Code's companion system generates your pet's visual traits (species, rari
21
23
 
22
24
  1. **You pick** your desired species, rarity, eyes, and hat through an interactive TUI
23
25
  2. **Brute-force search** finds a replacement salt string that produces your chosen pet when combined with your real user ID (typically takes <100ms)
24
- 3. **Binary patch** replaces the salt in the compiled Claude Code ELF binary (all 3 occurrences) using an atomic rename, with a backup created first
26
+ 3. **Binary patch** replaces the salt in the compiled Claude Code binary (all 3 occurrences) using an atomic rename, with a backup created first
25
27
  4. **Auto-repair hook** (optional) installs a `SessionStart` hook in `~/.claude/settings.json` that re-applies the patch after Claude Code updates
26
28
 
27
29
  The patch is safe — it uses `rename()` to atomically swap the binary, which is the same technique package managers use. A running Claude session continues using the old binary in memory; the new pet appears on next launch.
@@ -51,7 +53,7 @@ The interactive flow also lets you **rename** your companion — it edits `~/.cl
51
53
  ```bash
52
54
  # Clone and install
53
55
  git clone https://github.com/cpaczek/any-buddy.git
54
- cd claude-code-any-buddy
56
+ cd any-buddy
55
57
  pnpm install
56
58
 
57
59
  # Link globally (optional, enables the apply command for auto-patch hook)
@@ -61,7 +63,7 @@ pnpm link --global
61
63
  Or via npm:
62
64
 
63
65
  ```bash
64
- npm install -g claude-code-any-buddy
66
+ npm install -g any-buddy
65
67
  ```
66
68
 
67
69
  ## Usage
@@ -227,7 +229,7 @@ The hook adds negligible startup time (~50ms) when no patch is needed.
227
229
 
228
230
  ## How the binary patch works
229
231
 
230
- Claude Code is a compiled Bun ELF binary at `~/.local/share/claude/versions/<version>`. The salt string `"friend-2026-401"` appears exactly 3 times:
232
+ Claude Code is a compiled Bun binary (ELF on Linux, Mach-O on macOS) at `~/.local/share/claude/versions/<version>` (Linux) or the path shown by `which claude`. The salt string `"friend-2026-401"` appears exactly 3 times:
231
233
  - 2 occurrences in the bundled JavaScript code sections
232
234
  - 1 occurrence in a string table / data section
233
235
 
@@ -237,6 +239,7 @@ The patch:
237
239
  3. Replaces each with the new salt (always exactly 15 characters — same length, no byte offset shifts)
238
240
  4. Writes to a temp file, then atomically renames it over the original
239
241
  5. Verifies by re-reading
242
+ 6. On macOS, re-signs the binary with an ad-hoc signature (`codesign --force --sign -`)
240
243
 
241
244
  The atomic rename (`rename()` syscall) is safe even while Claude Code is running — the OS keeps the old inode open for any running process. The new binary takes effect on next launch.
242
245
 
@@ -262,7 +265,7 @@ This patches the salt back to the original, removes the SessionStart hook, and c
262
265
 
263
266
  ## Limitations
264
267
 
265
- - **Tested on Linux and macOS** — Windows should work but is not yet tested. Please [open an issue](https://github.com/cpaczek/any-buddy/issues) if you hit problems
268
+ - **Tested on Linux and macOS** — on macOS, the binary is automatically ad-hoc re-signed after patching to avoid code signature invalidation. Windows should work but is not yet tested. Please [open an issue](https://github.com/cpaczek/any-buddy/issues) if you hit problems
266
269
  - **Requires Bun** — needed for matching Claude Code's wyhash implementation
267
270
  - **Salt string dependent** — if Anthropic changes the salt from `friend-2026-401` in a future version, the patch logic would need updating (but the tool will detect this and warn you)
268
271
  - **Stats partially selectable** — you can pick which stat is highest (peak) and lowest (dump), but not exact values
package/lib/patcher.mjs CHANGED
@@ -175,6 +175,20 @@ export function isClaudeRunning(binaryPath) {
175
175
  }
176
176
  }
177
177
 
178
+ // Ad-hoc codesign on macOS (required after patching Mach-O binaries)
179
+ function codesignBinary(binaryPath) {
180
+ if (!IS_MAC) return { signed: false, error: null };
181
+ try {
182
+ execSync(`codesign --force --sign - "${binaryPath}"`, {
183
+ stdio: ['pipe', 'pipe', 'pipe'],
184
+ timeout: 30000,
185
+ });
186
+ return { signed: true, error: null };
187
+ } catch (err) {
188
+ return { signed: false, error: err.message };
189
+ }
190
+ }
191
+
178
192
  // Patch the binary: replace oldSalt with newSalt at all occurrences.
179
193
  export function patchBinary(binaryPath, oldSalt, newSalt) {
180
194
  if (oldSalt.length !== newSalt.length) {
@@ -239,10 +253,16 @@ export function patchBinary(binaryPath, oldSalt, newSalt) {
239
253
  // Verify
240
254
  const verifyBuf = readFileSync(binaryPath);
241
255
  const verify = findAllOccurrences(verifyBuf, newSalt);
256
+
257
+ // Re-sign on macOS (patching invalidates the Mach-O code signature)
258
+ const cs = codesignBinary(binaryPath);
259
+
242
260
  return {
243
261
  replacements: offsets.length,
244
262
  verified: verify.length === offsets.length,
245
263
  backupPath,
264
+ codesigned: cs.signed,
265
+ codesignError: cs.error,
246
266
  };
247
267
  }
248
268
 
package/lib/preflight.mjs CHANGED
@@ -4,7 +4,7 @@ import { platform } from 'os';
4
4
  import chalk from 'chalk';
5
5
  import { ORIGINAL_SALT } from './constants.mjs';
6
6
  import { findClaudeBinary, verifySalt } from './patcher.mjs';
7
- import { getClaudeUserId } from './config.mjs';
7
+ import { getClaudeUserId, loadPetConfig } from './config.mjs';
8
8
 
9
9
  const ISSUE_URL = 'https://github.com/cpaczek/any-buddy/issues';
10
10
 
@@ -60,22 +60,43 @@ export function runPreflight({ requireBinary = true } = {}) {
60
60
  }
61
61
  } catch { /* ignore */ }
62
62
 
63
- // Check that the salt exists in the binary
63
+ // Check that a known salt exists in the binary (original or previously patched)
64
64
  try {
65
- const result = verifySalt(binaryPath, ORIGINAL_SALT);
66
- saltCount = result.found;
65
+ const origResult = verifySalt(binaryPath, ORIGINAL_SALT);
66
+ saltCount = origResult.found;
67
67
 
68
68
  if (saltCount === 0) {
69
- // Check if it might already be patched with a different salt
70
- errors.push(
71
- `Salt "${ORIGINAL_SALT}" not found in ${binaryPath}.\n` +
72
- ' Possible reasons:\n' +
73
- ' - Binary is already patched with a custom salt (run `any-buddy restore` first)\n' +
74
- ' - This binary format doesn\'t contain the salt as a plain string\n' +
75
- ' - Claude Code changed the salt in a new version\n' +
76
- `\n Platform: ${platform()}, binary: ${binaryPath}` +
77
- `\n Please report this at: ${ISSUE_URL}`
78
- );
69
+ // Not original — check if it's already patched by any-buddy
70
+ const petConfig = loadPetConfig();
71
+ if (petConfig?.salt) {
72
+ const patchedResult = verifySalt(binaryPath, petConfig.salt);
73
+ if (patchedResult.found >= 2) {
74
+ // Already patched by us that's fine, we can re-patch
75
+ saltCount = patchedResult.found;
76
+ warnings.push(
77
+ `Binary is already patched with a custom salt from a previous run.\n` +
78
+ ' Re-patching will replace it with your new selection.'
79
+ );
80
+ } else {
81
+ errors.push(
82
+ `Neither the original salt nor your saved salt were found in ${binaryPath}.\n` +
83
+ ' The binary may have been updated or patched by another tool.\n' +
84
+ ' Try: any-buddy restore\n' +
85
+ `\n Platform: ${platform()}, binary: ${binaryPath}` +
86
+ `\n Please report this at: ${ISSUE_URL}`
87
+ );
88
+ }
89
+ } else {
90
+ errors.push(
91
+ `Salt "${ORIGINAL_SALT}" not found in ${binaryPath}.\n` +
92
+ ' Possible reasons:\n' +
93
+ ' - Binary was patched by another tool\n' +
94
+ ' - This binary format doesn\'t contain the salt as a plain string\n' +
95
+ ' - Claude Code changed the salt in a new version\n' +
96
+ `\n Platform: ${platform()}, binary: ${binaryPath}` +
97
+ `\n Please report this at: ${ISSUE_URL}`
98
+ );
99
+ }
79
100
  } else if (saltCount < 2) {
80
101
  warnings.push(
81
102
  `Salt found only ${saltCount} time(s) in binary (expected 3 on Linux).\n` +
@@ -100,9 +121,8 @@ export function runPreflight({ requireBinary = true } = {}) {
100
121
  );
101
122
  } else if (plat === 'darwin') {
102
123
  warnings.push(
103
- 'macOS support is experimental.\n' +
104
- ' If the binary is code-signed, patching may invalidate the signature.\n' +
105
- ' If Claude Code won\'t launch after patching, run `any-buddy restore`.\n' +
124
+ 'macOS: the binary will be ad-hoc re-signed after patching.\n' +
125
+ ' If Claude Code won\'t launch, run `any-buddy restore`.\n' +
106
126
  ' Please report issues at: ' + ISSUE_URL
107
127
  );
108
128
  }
package/lib/tui.mjs CHANGED
@@ -66,6 +66,13 @@ function showPet(bones, label = 'Your pet') {
66
66
  console.log();
67
67
  }
68
68
 
69
+ function warnCodesign(result, binaryPath) {
70
+ if (result.codesignError) {
71
+ console.log(chalk.yellow(` Warning: codesign failed: ${result.codesignError}`));
72
+ console.log(chalk.yellow(` Run manually: codesign --force --sign - "${binaryPath}"`));
73
+ }
74
+ }
75
+
69
76
  // ─── Subcommands ───
70
77
 
71
78
  export async function runCurrent() {
@@ -137,7 +144,10 @@ export async function runApply({ silent = false } = {}) {
137
144
  const prevCheck = verifySalt(binaryPath, config.previousSalt);
138
145
  if (prevCheck.found >= 3) {
139
146
  const result = patchBinary(binaryPath, config.previousSalt, config.salt);
140
- if (!silent) console.log(chalk.green(` Re-patched (${result.replacements} replacements).`));
147
+ if (!silent) {
148
+ console.log(chalk.green(` Re-patched (${result.replacements} replacements).`));
149
+ warnCodesign(result, binaryPath);
150
+ }
141
151
  return;
142
152
  }
143
153
  }
@@ -145,7 +155,10 @@ export async function runApply({ silent = false } = {}) {
145
155
  const origCheck = verifySalt(binaryPath, ORIGINAL_SALT);
146
156
  if (origCheck.found >= 3) {
147
157
  const result = patchBinary(binaryPath, ORIGINAL_SALT, config.salt);
148
- if (!silent) console.log(chalk.green(` Patched after update (${result.replacements} replacements).`));
158
+ if (!silent) {
159
+ console.log(chalk.green(` Patched after update (${result.replacements} replacements).`));
160
+ warnCodesign(result, binaryPath);
161
+ }
149
162
  return;
150
163
  }
151
164
  if (!silent) console.error('Could not find known salt in binary. Claude Code may have changed the salt string.');
@@ -155,6 +168,7 @@ export async function runApply({ silent = false } = {}) {
155
168
  const result = patchBinary(binaryPath, oldSalt, config.salt);
156
169
  if (!silent) {
157
170
  console.log(chalk.green(` Applied (${result.replacements} replacements).`));
171
+ warnCodesign(result, binaryPath);
158
172
  if (isClaudeRunning(binaryPath)) {
159
173
  console.log(chalk.yellow(' Restart Claude Code for the change to take effect.'));
160
174
  }
@@ -170,8 +184,9 @@ export async function runRestore() {
170
184
  // Try to patch back to original
171
185
  const check = verifySalt(binaryPath, config.salt);
172
186
  if (check.found >= 3) {
173
- patchBinary(binaryPath, config.salt, ORIGINAL_SALT);
187
+ const restoreResult = patchBinary(binaryPath, config.salt, ORIGINAL_SALT);
174
188
  console.log(chalk.green(' Restored original pet salt.'));
189
+ warnCodesign(restoreResult, binaryPath);
175
190
  } else {
176
191
  // Try backup
177
192
  try {
@@ -364,6 +379,11 @@ export async function runInteractive(flags = {}) {
364
379
 
365
380
  const patchResult = patchBinary(binaryPath, oldSalt, result.salt);
366
381
  console.log(chalk.green(` Patched! ${patchResult.replacements} replacements, verified: ${patchResult.verified}`));
382
+ if (patchResult.codesigned) {
383
+ console.log(chalk.dim(` Re-signed for macOS.`));
384
+ } else {
385
+ warnCodesign(patchResult, binaryPath);
386
+ }
367
387
  console.log(chalk.dim(` Backup: ${patchResult.backupPath}`));
368
388
 
369
389
  // Save config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-buddy",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Pick any Claude Code companion pet you want",
5
5
  "type": "module",
6
6
  "bin": {