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 +9 -6
- package/lib/patcher.mjs +20 -0
- package/lib/preflight.mjs +37 -17
- package/lib/tui.mjs +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
// Check that a known salt exists in the binary (original or previously patched)
|
|
64
64
|
try {
|
|
65
|
-
const
|
|
66
|
-
saltCount =
|
|
65
|
+
const origResult = verifySalt(binaryPath, ORIGINAL_SALT);
|
|
66
|
+
saltCount = origResult.found;
|
|
67
67
|
|
|
68
68
|
if (saltCount === 0) {
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
104
|
-
' If
|
|
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)
|
|
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)
|
|
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
|