Files
clawgod/install.sh
T
0chencc 2cfa74e7c0 feat: native binary support for v2.1.113+ Claude Code
Anthropic stopped shipping cli.js in the npm package starting v2.1.113;
the package became a thin loader that dispatches to a per-platform Bun
standalone binary. The previous installer cp'd cli.js from the npm
tarball, which is why every install on v2.1.113+ fails with ENOENT.

This release rewrites the install pipeline to extract cli.js source
directly from the user's locally-installed Bun standalone binary
(__BUN segment, anchored on the file:///$bunfs/.../cli.js path),
rewrites virtual /$bunfs paths to local vendor/, runs cli.js under Bun
runtime instead of Node, and reapplies the existing 23 regex patches.

Highlights

- Extract cli.js from Mach-O / ELF / PE __BUN segment (the same source
  Bun bundled at build time — not stale, not injected).
- Auto-install Bun runtime; require ripgrep up front (Bun's bundled rg
  is unreachable when running extracted cli.js outside the standalone
  bundle, so we depend on system rg).
- Wrapper now stamps the source version and detects drift on every
  launch. When the user upgrades Claude Code, ClawGod transparently
  re-extracts and re-patches before invoking the user's command.
- Patcher made resilient: regex tokens widened to [\\w$]+ for Bun's
  newer minified identifiers; updated message-list filter and
  auto-mode patches for v2.1.119 source shape; sentinel-based
  verification catches future silent regex misses.
- New `clawgod` command alongside `claude` — unambiguous entry point
  that survives Windows .exe / .cmd shadowing and official
  self-update overwriting `claude`.
- Critical fix: launcher write no longer follows symlinks. Previously
  `echo > $CLAUDE_BIN` clobbered the real binary when $CLAUDE_BIN was
  the official symlink to ~/.local/share/claude/versions/<v>; we now
  rm the entry first.
- Default features.json adds tengu_prompt_cache_1h_config allowlist
  so 1h prompt cache actually engages (fixes silent 5m TTL fallback
  that drove cache_creation token waste).

Verified: macOS arm64 against v2.1.117/2.1.118/2.1.119 native binaries;
23 patches applied with 0 failures; auto-repatch correctly handles
version transitions; claude / clawgod / claude.orig coexist cleanly.

Closes #40
Closes #41
Closes #42
Closes #43
Closes #45
2026-04-27 15:14:44 +09:00

1258 lines
46 KiB
Bash
Executable File

#!/bin/bash
set -e
# ─────────────────────────────────────────────────────────
# ClawGod Installer
#
# Downloads Claude Code from npm, applies patches, replaces claude command
#
# 用法:
# curl -fsSL https://raw.githubusercontent.com/0Chencc/clawgod/main/install.sh | bash
# # 或
# bash install.sh [--version 2.1.89]
# ─────────────────────────────────────────────────────────
CLAWGOD_DIR="$HOME/.clawgod"
BIN_DIR="$HOME/.local/bin"
VERSION="${CLAWGOD_VERSION:-latest}"
# Parse args
while [[ $# -gt 0 ]]; do
case $1 in
--version) VERSION="$2"; shift 2 ;;
--uninstall) UNINSTALL=1; shift ;;
*) shift ;;
esac
done
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
DIM='\033[2m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e " ${GREEN}✓${NC} $1"; }
warn() { echo -e " ${RED}✗${NC} $1"; }
dim() { echo -e " ${DIM}$1${NC}"; }
echo ""
echo -e "${BOLD} ClawGod Installer${NC}"
echo ""
# ─── Uninstall ─────────────────────────────────────────
if [ "$UNINSTALL" = "1" ]; then
CLAUDE_BIN=$(which claude 2>/dev/null)
for DIR in "${CLAUDE_BIN:+$(dirname "$CLAUDE_BIN")}" "$BIN_DIR"; do
[ -z "$DIR" ] && continue
if [ -e "$DIR/claude.orig" ]; then
# Has backup — restore it
mv "$DIR/claude.orig" "$DIR/claude"
info "Original claude restored ($DIR/claude)"
elif [ -f "$DIR/claude" ] && grep -q "clawgod" "$DIR/claude" 2>/dev/null; then
# Our launcher, no backup — remove it (otherwise it points to deleted cli.js)
rm -f "$DIR/claude"
info "Removed ClawGod launcher ($DIR/claude)"
fi
# Always remove the explicit clawgod alias if it's ours
if [ -f "$DIR/clawgod" ] && grep -q "clawgod" "$DIR/clawgod" 2>/dev/null; then
rm -f "$DIR/clawgod"
info "Removed ClawGod alias ($DIR/clawgod)"
fi
done
rm -rf "$CLAWGOD_DIR/node_modules" "$CLAWGOD_DIR/vendor" "$CLAWGOD_DIR/cli.original.js" "$CLAWGOD_DIR/cli.original.js.bak" "$CLAWGOD_DIR/cli.original.cjs" "$CLAWGOD_DIR/cli.original.cjs.bak" "$CLAWGOD_DIR/cli.js" "$CLAWGOD_DIR/cli.cjs" "$CLAWGOD_DIR/patch.mjs" "$CLAWGOD_DIR/patch.js" "$CLAWGOD_DIR/extract-natives.mjs" "$CLAWGOD_DIR/post-process.mjs" "$CLAWGOD_DIR/repatch.mjs" "$CLAWGOD_DIR/.source-version"
hash -r 2>/dev/null
info "ClawGod uninstalled"
echo ""
warn " Restart your terminal or run: hash -r"
echo ""
exit 0
fi
# ─── Prerequisites ─────────────────────────────────────
if ! command -v node &>/dev/null; then
warn "Node.js is required (>= 18) for the patcher. Install from https://nodejs.org"
exit 1
fi
NODE_VERSION=$(node -e "console.log(process.versions.node.split('.')[0])")
if [ "$NODE_VERSION" -lt 18 ]; then
warn "Node.js >= 18 required (found v$NODE_VERSION)"
exit 1
fi
# ─── Ensure Bun (runtime that executes the patched cli.js) ─────────────
BUN_BIN=""
if command -v bun &>/dev/null; then
BUN_BIN=$(command -v bun)
elif [ -x "$HOME/.bun/bin/bun" ]; then
BUN_BIN="$HOME/.bun/bin/bun"
else
dim "Installing Bun (required runtime for v2.1.113+ cli.js) ..."
curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || true
BUN_BIN="$HOME/.bun/bin/bun"
if [ ! -x "$BUN_BIN" ]; then
warn "Bun installation failed. Install manually: https://bun.sh/install"
exit 1
fi
fi
info "Bun: $($BUN_BIN --version)"
# ─── ripgrep prerequisite (search/grep tool) ──────────────────────────
# Without rg the Grep tool inside Claude Code fails. Bun-bundled ripgrep
# is only reachable from inside the standalone executable; running the
# extracted cli.js under Bun runtime means we depend on system rg.
# This is a hard prerequisite — refuse to install otherwise.
if ! command -v rg &>/dev/null; then
warn "ripgrep (rg) is required but not found in PATH."
warn " Claude Code's Grep tool will not function without it."
warn ""
case "$(uname -s)" in
Darwin) warn " Install: brew install ripgrep" ;;
Linux) warn " Install: apt install ripgrep | dnf install ripgrep | pacman -S ripgrep" ;;
*) warn " Install: https://github.com/BurntSushi/ripgrep#installation" ;;
esac
warn ""
warn " Re-run this script after installing rg."
exit 1
fi
info "ripgrep: $(rg --version | head -1)"
# ─── Locate native Bun binary (must be present — cli.js source) ───────
# v2.1.113+ ships a Bun standalone executable as the only canonical form.
# We extract cli.js text from this binary, patch it, then run via Bun
# runtime. The binary itself comes from the user's prior Claude Code
# installation (e.g. via the official `claude.ai/install.sh`).
mkdir -p "$CLAWGOD_DIR" "$BIN_DIR"
NATIVE_BIN=""
VERSIONS_DIR="$HOME/.local/share/claude/versions"
if [ -d "$VERSIONS_DIR" ]; then
for f in $(ls -t "$VERSIONS_DIR"/* 2>/dev/null); do
if file "$f" 2>/dev/null | grep -qE "Mach-O|ELF"; then
NATIVE_BIN="$f"
break
fi
done
fi
# Fallback: backed-up .orig from a prior clawgod install
if [ -z "$NATIVE_BIN" ] && [ -e "$BIN_DIR/claude.orig" ]; then
if file "$BIN_DIR/claude.orig" 2>/dev/null | grep -qE "Mach-O|ELF"; then
NATIVE_BIN="$BIN_DIR/claude.orig"
elif [ -L "$BIN_DIR/claude.orig" ]; then
NATIVE_BIN="$(readlink "$BIN_DIR/claude.orig")"
fi
fi
if [ -z "$NATIVE_BIN" ]; then
warn "Native Claude Code binary not found in $VERSIONS_DIR"
warn "Install the official binary first:"
warn " curl -fsSL https://claude.ai/install.sh | bash"
warn "Then re-run this script."
exit 1
fi
# Write extractor to a temp file (used both for cli.js and .node modules)
cat > "$CLAWGOD_DIR/extract-natives.mjs" << 'EXTRACTOR_EOF'
#!/usr/bin/env node
/**
* ClawGod native module extractor
*
* Extracts embedded .node NAPI modules from a Bun single-file executable
* (the official Claude Code native binary).
*
* Supports:
* - Mach-O (macOS) — arm64 + x86_64 thin binaries
* - ELF (Linux) — arm64 + x86_64
* - PE (Windows) — x86_64 + arm64
*
* Usage:
* node extract-natives.mjs <binary-path> <output-dir>
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
import { join, basename } from 'path';
// ─── Mach-O constants ────────────────────────────────────────────────
const MH_MAGIC_64 = 0xfeedfacf; // little-endian 64-bit
const LC_SEGMENT_64 = 0x19;
const LC_ID_DYLIB = 0x0d;
const MH_DYLIB = 6;
const CPU_TYPE_X86_64 = 0x01000007;
const CPU_TYPE_ARM64 = 0x0100000c;
// ─── ELF constants ───────────────────────────────────────────────────
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // 7f 'E' 'L' 'F'
const ET_DYN = 3; // shared object
const EM_X86_64 = 62;
const EM_AARCH64 = 183;
// ─── PE constants ────────────────────────────────────────────────────
const MZ_MAGIC = Buffer.from([0x4d, 0x5a]); // "MZ"
const PE_MAGIC = Buffer.from([0x50, 0x45, 0, 0]); // "PE\0\0"
const IMAGE_FILE_MACHINE_AMD64 = 0x8664;
const IMAGE_FILE_MACHINE_ARM64 = 0xaa64;
const IMAGE_FILE_DLL = 0x2000;
// ─── Helpers ─────────────────────────────────────────────────────────
function archName(format, cputype) {
if (format === 'macho') {
if (cputype === CPU_TYPE_ARM64) return 'arm64';
if (cputype === CPU_TYPE_X86_64) return 'x64';
}
if (format === 'elf') {
if (cputype === EM_AARCH64) return 'arm64';
if (cputype === EM_X86_64) return 'x64';
}
if (format === 'pe') {
if (cputype === IMAGE_FILE_MACHINE_ARM64) return 'arm64';
if (cputype === IMAGE_FILE_MACHINE_AMD64) return 'x64';
}
return null;
}
function platformSuffix(format, arch) {
const os = format === 'macho' ? 'darwin' : format === 'elf' ? 'linux' : 'win32';
return `${arch}-${os}`;
}
// ─── Mach-O parser ───────────────────────────────────────────────────
function parseMachODylib(buf, off) {
const magic = buf.readUInt32LE(off);
if (magic !== MH_MAGIC_64) return null;
const cputype = buf.readUInt32LE(off + 4);
if (cputype !== CPU_TYPE_ARM64 && cputype !== CPU_TYPE_X86_64) return null;
const filetype = buf.readUInt32LE(off + 12);
if (filetype !== MH_DYLIB) return null;
const ncmds = buf.readUInt32LE(off + 16);
if (ncmds === 0 || ncmds > 500) return null;
let totalFileEnd = 0;
let installName = null;
let cmdOff = off + 32;
for (let i = 0; i < ncmds; i++) {
if (cmdOff + 8 > buf.length) return null;
const cmd = buf.readUInt32LE(cmdOff);
const cmdsize = buf.readUInt32LE(cmdOff + 4);
if (cmdsize === 0 || cmdsize > 65536) return null;
if (cmd === LC_SEGMENT_64) {
const fileoff = Number(buf.readBigUInt64LE(cmdOff + 40));
const filesize = Number(buf.readBigUInt64LE(cmdOff + 48));
const end = fileoff + filesize;
if (end > totalFileEnd) totalFileEnd = end;
} else if (cmd === LC_ID_DYLIB) {
// dylib_command: uint32 cmd, cmdsize, str_offset, timestamp, version...
// then name string at cmdOff + str_offset
const strOff = buf.readUInt32LE(cmdOff + 8);
const nameStart = cmdOff + strOff;
const nameEnd = buf.indexOf(0, nameStart);
if (nameEnd !== -1 && nameEnd - nameStart < 1024) {
installName = buf.slice(nameStart, nameEnd).toString('utf8');
}
}
cmdOff += cmdsize;
}
if (totalFileEnd === 0) return null;
return {
offset: off,
size: totalFileEnd,
arch: archName('macho', cputype),
installName,
};
}
function extractMachODylibs(buf) {
const dylibs = [];
// Magic bytes for fast indexOf scan: cf fa ed fe (MH_MAGIC_64 LE)
const magicBytes = Buffer.from([0xcf, 0xfa, 0xed, 0xfe]);
let off = 1; // skip the main binary at offset 0
while ((off = buf.indexOf(magicBytes, off)) !== -1) {
const info = parseMachODylib(buf, off);
if (info && off + info.size <= buf.length) {
dylibs.push(info);
off += info.size; // skip past this dylib
} else {
off += 4;
}
}
return dylibs;
}
// ─── ELF parser ──────────────────────────────────────────────────────
function parseELFSharedObject(buf, off) {
if (buf.length - off < 64) return null;
if (!buf.slice(off, off + 4).equals(ELF_MAGIC)) return null;
const eiClass = buf.readUInt8(off + 4); // 1=32-bit, 2=64-bit
if (eiClass !== 2) return null;
const eiData = buf.readUInt8(off + 5); // 1=LE, 2=BE
if (eiData !== 1) return null; // only LE supported
const eType = buf.readUInt16LE(off + 16);
if (eType !== ET_DYN) return null;
const eMachine = buf.readUInt16LE(off + 18);
if (eMachine !== EM_X86_64 && eMachine !== EM_AARCH64) return null;
// ELF64 header layout:
// e_shoff (section header offset): off + 40 (u64)
// e_shentsize: off + 58 (u16)
// e_shnum: off + 60 (u16)
const shoff = Number(buf.readBigUInt64LE(off + 40));
const shentsize = buf.readUInt16LE(off + 58);
const shnum = buf.readUInt16LE(off + 60);
if (shentsize !== 64 || shnum === 0 || shnum > 1000) return null;
// Total size = shoff + shnum * shentsize (the section header table is at the end)
const totalSize = shoff + shnum * shentsize;
if (totalSize > buf.length - off) return null;
return {
offset: off,
size: totalSize,
arch: archName('elf', eMachine),
installName: null, // ELF soname requires dynamic section walk; we'll rely on adjacent strings
};
}
function extractELFSharedObjects(buf) {
const sos = [];
// Scan for ELF magic; ELF headers are rare in data so 4-byte alignment is fine
for (let off = 4; off < buf.length - 64; off += 4) {
if (buf.readUInt8(off) !== 0x7f) continue;
const info = parseELFSharedObject(buf, off);
if (!info) continue;
if (off + info.size > buf.length) continue;
sos.push(info);
}
return sos;
}
// ─── PE parser ───────────────────────────────────────────────────────
function parsePEDll(buf, off) {
if (buf.length - off < 1024) return null;
if (!buf.slice(off, off + 2).equals(MZ_MAGIC)) return null;
// PE header offset at MZ + 0x3c (e_lfanew)
const peOff = buf.readUInt32LE(off + 0x3c);
if (peOff > 4096) return null; // sanity
if (off + peOff + 24 > buf.length) return null;
if (!buf.slice(off + peOff, off + peOff + 4).equals(PE_MAGIC)) return null;
const machine = buf.readUInt16LE(off + peOff + 4);
if (machine !== IMAGE_FILE_MACHINE_AMD64 && machine !== IMAGE_FILE_MACHINE_ARM64) return null;
const numberOfSections = buf.readUInt16LE(off + peOff + 6);
const sizeOfOptionalHeader = buf.readUInt16LE(off + peOff + 20);
const characteristics = buf.readUInt16LE(off + peOff + 22);
if (!(characteristics & IMAGE_FILE_DLL)) return null;
// Walk sections to find the max (PointerToRawData + SizeOfRawData)
const sectionHeaderOff = off + peOff + 24 + sizeOfOptionalHeader;
let totalSize = sectionHeaderOff - off; // header area minimum
for (let i = 0; i < numberOfSections; i++) {
const secOff = sectionHeaderOff + i * 40;
if (secOff + 40 > buf.length) return null;
const sizeOfRawData = buf.readUInt32LE(secOff + 16);
const pointerToRawData = buf.readUInt32LE(secOff + 20);
const end = pointerToRawData + sizeOfRawData;
if (end > totalSize) totalSize = end;
}
if (totalSize === 0 || totalSize > 50 * 1024 * 1024) return null;
return {
offset: off,
size: totalSize,
arch: archName('pe', machine),
installName: null,
};
}
function extractPEDlls(buf) {
const dlls = [];
for (let off = 0; off < buf.length - 1024; off++) {
if (buf.readUInt8(off) !== 0x4d) continue;
if (buf.readUInt8(off + 1) !== 0x5a) continue;
const info = parsePEDll(buf, off);
if (!info) continue;
if (off + info.size > buf.length) continue;
dlls.push(info);
}
return dlls;
}
// ─── Main dispatch ───────────────────────────────────────────────────
function detectFormat(buf) {
if (buf.readUInt32LE(0) === MH_MAGIC_64) return 'macho';
if (buf.slice(0, 4).equals(ELF_MAGIC)) return 'elf';
if (buf.slice(0, 2).equals(MZ_MAGIC)) return 'pe';
return null;
}
// Names to look for from install names / nearby strings
const KNOWN_MODULES = [
'image-processor',
'audio-capture',
'computer-use-input',
'computer-use-swift',
'url-handler',
];
function identifyDylib(buf, dylib) {
// 1. Try install name (most reliable)
if (dylib.installName) {
const base = basename(dylib.installName).replace(/\.(node|dylib|so|dll)$/, '');
for (const m of KNOWN_MODULES) {
if (base === m) return m;
// Handle variants like "libcomputer_use_input.dylib"
if (base === `lib${m.replace(/-/g, '_')}`) return m;
if (base === `lib${m.replace(/-/g, '')}`) return m;
if (base.toLowerCase().includes(m.replace(/-/g, ''))) return m;
}
}
// 2. Scan the dylib body for known module name strings
const body = buf.slice(dylib.offset, dylib.offset + dylib.size);
for (const m of KNOWN_MODULES) {
if (body.indexOf(Buffer.from(m)) !== -1) return m;
}
return null;
}
// ─── cli.js text extraction (Bun standalone) ─────────────────────────
const CLI_PATH_MARKER = Buffer.from('file:///$bunfs/root/src/entrypoints/cli.js');
const CLI_FN_MARKER = Buffer.from('(function(exports, require, module');
const CLI_TAIL_MARKER = Buffer.from('cli_after_main_complete")}');
const CLI_END_MARKER = Buffer.from(');})');
function extractCliJs(buf) {
const pathOff = buf.indexOf(CLI_PATH_MARKER);
if (pathOff === -1) return null;
const fnStart = buf.indexOf(CLI_FN_MARKER, pathOff);
if (fnStart === -1 || fnStart - pathOff > 1024) return null;
const tailMark = buf.indexOf(CLI_TAIL_MARKER, fnStart);
if (tailMark === -1) return null;
const ending = buf.indexOf(CLI_END_MARKER, tailMark);
if (ending === -1 || ending - tailMark > 4096) return null;
return buf.slice(fnStart, ending + CLI_END_MARKER.length).toString('utf8');
}
function main() {
const [, , binaryPath, outputDir, ...rest] = process.argv;
const wantCliJs = rest.includes('--cli-js');
if (!binaryPath || !outputDir) {
console.error('Usage: extract-natives.mjs <binary-path> <output-dir> [--cli-js]');
process.exit(1);
}
if (!existsSync(binaryPath)) {
console.error(`Binary not found: ${binaryPath}`);
process.exit(1);
}
const stat = statSync(binaryPath);
if (stat.size < 10 * 1024 * 1024) {
console.error(`Binary too small (${stat.size} bytes) — not a native Claude Code binary`);
process.exit(1);
}
const buf = readFileSync(binaryPath);
const format = detectFormat(buf);
if (!format) {
console.error('Unknown binary format (expected Mach-O / ELF / PE)');
process.exit(1);
}
console.log(`Format: ${format}`);
console.log(`Size: ${(buf.length / 1024 / 1024).toFixed(1)} MB`);
if (wantCliJs) {
const js = extractCliJs(buf);
if (!js) {
console.error('Could not locate cli.js payload in binary (markers missing).');
process.exit(2);
}
mkdirSync(outputDir, { recursive: true });
const out = join(outputDir, 'cli.original.js');
writeFileSync(out, js);
console.log(` cli.js ${(js.length / 1024 / 1024).toFixed(2)} MB → ${out}`);
return;
}
let libs = [];
if (format === 'macho') libs = extractMachODylibs(buf);
else if (format === 'elf') libs = extractELFSharedObjects(buf);
else if (format === 'pe') libs = extractPEDlls(buf);
// Skip the first (main binary itself)
libs = libs.filter(l => l.offset !== 0);
console.log(`Found: ${libs.length} embedded native libraries`);
console.log();
mkdirSync(outputDir, { recursive: true });
const summary = { extracted: [], skipped: [] };
for (const lib of libs) {
const name = identifyDylib(buf, lib);
if (!name) {
summary.skipped.push({ ...lib, reason: 'unidentified' });
continue;
}
const platform = platformSuffix(format, lib.arch);
const targetDir = join(outputDir, name, platform);
mkdirSync(targetDir, { recursive: true });
const targetFile = join(targetDir, `${name}.node`);
const data = buf.slice(lib.offset, lib.offset + lib.size);
writeFileSync(targetFile, data);
console.log(` ✓ ${name.padEnd(20)} ${lib.arch.padEnd(6)} ${(lib.size / 1024).toFixed(0).padStart(5)} KB → ${targetFile}`);
summary.extracted.push({ name, platform, size: lib.size });
}
console.log();
console.log(`Extracted ${summary.extracted.length}, skipped ${summary.skipped.length}`);
if (summary.skipped.length > 0) {
console.log('\nSkipped (unidentified):');
for (const s of summary.skipped) {
console.log(` offset=${s.offset} arch=${s.arch} size=${(s.size / 1024).toFixed(0)}KB`);
}
}
}
main();
EXTRACTOR_EOF
# ─── Extract cli.js + native modules from Bun binary ──────────
# Note: extract-natives.mjs and post-process.mjs are kept around (NOT deleted)
# so the wrapper's drift detector can re-run them when the user upgrades
# their native Claude binary.
VENDOR_DIR="$CLAWGOD_DIR/vendor"
rm -rf "$VENDOR_DIR" 2>/dev/null
mkdir -p "$VENDOR_DIR"
dim "Extracting cli.js from $(basename "$NATIVE_BIN") ..."
if ! node "$CLAWGOD_DIR/extract-natives.mjs" "$NATIVE_BIN" "$CLAWGOD_DIR" --cli-js 2>&1 | while IFS= read -r line; do echo " $line"; done; then
err "Failed to extract cli.js from native binary"
exit 1
fi
[ -f "$CLAWGOD_DIR/cli.original.js" ] || { err "cli.js missing after extraction"; exit 1; }
dim "Extracting native modules from $(basename "$NATIVE_BIN") ..."
node "$CLAWGOD_DIR/extract-natives.mjs" "$NATIVE_BIN" "$VENDOR_DIR" 2>&1 | while IFS= read -r line; do echo " $line"; done || true
# ─── Post-process cli.js for Bun runtime ──────────────────────
# 1. Rewrite /$bunfs/root/X.node paths to point at extracted vendor modules
# 2. Rewrite build-time /home/runner/.../*.ts URLs (used by ripgrep,
# sandbox, computer-use, etc. for asset resolution) to __filename so
# relative resolutions land near our cli.original.cjs
# 3. Wrap the Bun-cjs IIFE with an actual invocation so `require()` runs it
# 4. Save as .cjs (Bun + CJS module wrapper)
dim "Rewriting bunfs paths and IIFE invocation ..."
cat > "$CLAWGOD_DIR/post-process.mjs" << 'POSTPROC_EOF'
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const here = dirname(fileURLToPath(import.meta.url));
const src = `${here}/cli.original.js`;
const dst = `${here}/cli.original.cjs`;
let code = readFileSync(src, 'utf8');
// (1) bunfs .node module paths → runtime vendor lookup
code = code.replace(
/require\(['"](\/\$bunfs\/root\/([\w-]+)\.node)['"]\)/g,
(m, _full, name) =>
`require(require('path').join(__dirname,'vendor',${JSON.stringify(name)},\`\${process.arch==='arm64'?'arm64':'x64'}-\${process.platform==='darwin'?'darwin':process.platform==='linux'?'linux':'win32'}\`,${JSON.stringify(name + '.node')}))`,
);
// (2) build-time fileURLToPath() leaks → use cli.cjs's own __filename
code = code.replace(
/[\w$]+\.fileURLToPath\("file:\/\/\/home\/runner\/work\/claude-cli-internal\/claude-cli-internal\/[^"]*"\)/g,
() => '__filename',
);
// (3) make the outer (function(...){...}) actually run
code = code.replace(/\}\)\s*$/, '})(exports, require, module, __filename, __dirname)');
writeFileSync(dst, code);
unlinkSync(src);
console.log(`cli.original.cjs: ${code.length} bytes`);
POSTPROC_EOF
node "$CLAWGOD_DIR/post-process.mjs" 2>&1 | while IFS= read -r line; do echo " $line"; done
[ -f "$CLAWGOD_DIR/cli.original.cjs" ] || { err "Post-process failed"; exit 1; }
# Stamp the source version so the wrapper can detect drift on next launch
echo "$(basename "$NATIVE_BIN")" > "$CLAWGOD_DIR/.source-version"
info "cli.original.cjs ready ($(basename "$NATIVE_BIN"))"
# ─── Write re-patch helper (used by wrapper on version drift) ─────────
cat > "$CLAWGOD_DIR/repatch.mjs" << 'REPATCH_EOF'
#!/usr/bin/env bun
// Re-extract + post-process + patch the user's currently-installed
// native Claude binary. Invoked by cli.cjs when it detects that
// .source-version no longer matches the latest binary in versions/.
import { spawnSync } from 'child_process';
import { writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
import { dirname, join, basename } from 'path';
import { fileURLToPath } from 'url';
const here = dirname(fileURLToPath(import.meta.url));
const nativeBin = process.argv[2];
if (!nativeBin || !existsSync(nativeBin)) {
console.error('repatch: native binary path required and must exist');
process.exit(1);
}
const vendor = join(here, 'vendor');
rmSync(vendor, { recursive: true, force: true });
mkdirSync(vendor, { recursive: true });
const runtime = process.execPath;
function run(label, args) {
const r = spawnSync(runtime, args, { cwd: here, stdio: 'inherit' });
if (r.status !== 0) {
console.error(`repatch: ${label} failed (exit ${r.status})`);
process.exit(1);
}
}
const extractor = join(here, 'extract-natives.mjs');
const postProc = join(here, 'post-process.mjs');
const patcher = join(here, 'patch.mjs');
run('extract cli.js', [extractor, nativeBin, here, '--cli-js']);
run('extract natives', [extractor, nativeBin, vendor]);
run('post-process', [postProc]);
run('patcher', [patcher]);
writeFileSync(join(here, '.source-version'), basename(nativeBin) + '\n');
console.log(`[clawgod] re-patched to ${basename(nativeBin)}`);
REPATCH_EOF
chmod +x "$CLAWGOD_DIR/repatch.mjs"
info "Re-patch helper installed (repatch.mjs)"
# ─── Write wrapper (cli.cjs, runs under Bun) ──────────────────
cat > "$CLAWGOD_DIR/cli.cjs" << 'WRAPPER_EOF'
#!/usr/bin/env bun
const { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync } = require('fs');
const { join, basename } = require('path');
const { homedir } = require('os');
const { spawnSync } = require('child_process');
const claudeDir = join(homedir(), '.claude');
const clawgodDir = join(homedir(), '.clawgod');
// ─── Version drift detection ────────────────────────────────────────
// If the user upgraded their native Claude binary since we patched, the
// snapshot in cli.original.cjs is stale. Compare the basename of the most
// recent binary in ~/.local/share/claude/versions/ against what we stamped
// at install time, and silently re-patch if they differ. Resolves stamp
// + repatch script relative to this wrapper (__dirname) so it works
// regardless of where the user installed clawgod.
try {
const stamp = join(__dirname, '.source-version');
const versionsDir = join(homedir(), '.local/share/claude/versions');
if (existsSync(versionsDir)) {
let latest = null, latestMtime = 0;
for (const name of readdirSync(versionsDir)) {
const p = join(versionsDir, name);
let st;
try { st = statSync(p); } catch { continue; }
if (!st.isFile() || st.size < 10 * 1024 * 1024) continue;
if (st.mtimeMs > latestMtime) { latestMtime = st.mtimeMs; latest = p; }
}
if (latest) {
const have = existsSync(stamp) ? readFileSync(stamp, 'utf8').trim() : '';
const want = basename(latest);
if (have && have !== want) {
process.stderr.write(`[clawgod] new Claude version detected (${want}, was ${have}); re-patching...\n`);
const repatch = join(__dirname, 'repatch.mjs');
if (existsSync(repatch)) {
const r = spawnSync(process.execPath, [repatch, latest], { stdio: 'inherit' });
if (r.status !== 0) {
process.stderr.write('[clawgod] re-patch failed; running previous patched copy\n');
}
}
}
}
}
} catch (e) {
process.stderr.write(`[clawgod] drift check skipped: ${e.message}\n`);
}
// ────────────────────────────────────────────────────────────────────
const configDir = process.env.CLAUDE_CONFIG_DIR || (existsSync(claudeDir) ? claudeDir : clawgodDir);
const providerDir = clawgodDir;
const configFile = join(providerDir, 'provider.json');
const defaultConfig = {
apiKey: '',
baseURL: 'https://api.anthropic.com',
model: '',
smallModel: '',
timeoutMs: 3000000,
};
let config = { ...defaultConfig };
if (existsSync(configFile)) {
try {
const raw = JSON.parse(readFileSync(configFile, 'utf8'));
config = { ...defaultConfig, ...raw };
} catch {}
} else {
mkdirSync(providerDir, { recursive: true });
writeFileSync(configFile, JSON.stringify(defaultConfig, null, 2) + '\n');
}
const hasProviderApiKey = !!config.apiKey;
if (hasProviderApiKey) {
process.env.ANTHROPIC_API_KEY = config.apiKey;
if (config.baseURL) process.env.ANTHROPIC_BASE_URL = config.baseURL;
if (config.model) process.env.ANTHROPIC_MODEL = config.model;
if (config.smallModel) process.env.ANTHROPIC_SMALL_FAST_MODEL = config.smallModel;
process.env.CLAUDE_CONFIG_DIR = clawgodDir;
if (config.baseURL && !/anthropic\.com/i.test(config.baseURL)) {
process.env.ANTHROPIC_AUTH_TOKEN ??= config.apiKey;
}
} else {
if (config.baseURL && config.baseURL !== defaultConfig.baseURL) {
process.env.ANTHROPIC_BASE_URL ??= config.baseURL;
}
process.env.CLAUDE_CONFIG_DIR ??= configDir;
}
if (config.timeoutMs) {
process.env.API_TIMEOUT_MS ??= String(config.timeoutMs);
}
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC ??= '1';
process.env.DISABLE_INSTALLATION_CHECKS ??= '1';
// Use system ripgrep (extracted vendor rg path was build-time-baked; system
// rg is the most reliable fallback under Bun runtime).
process.env.USE_BUILTIN_RIPGREP ??= '1';
const featuresFile = join(providerDir, 'features.json');
if (!process.env.CLAUDE_INTERNAL_FC_OVERRIDES && existsSync(featuresFile)) {
try {
const raw = readFileSync(featuresFile, 'utf8');
JSON.parse(raw);
process.env.CLAUDE_INTERNAL_FC_OVERRIDES = raw;
} catch {}
}
require('./cli.original.cjs');
WRAPPER_EOF
chmod +x "$CLAWGOD_DIR/cli.cjs"
info "Wrapper created (cli.cjs)"
# ─── Write universal patcher ───────────────────────────
cat > "$CLAWGOD_DIR/patch.mjs" << 'PATCHER_EOF'
#!/usr/bin/env node
/**
* ClawGod Universal Patcher — 正则模式匹配, 跨版本兼容
*/
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TARGET = join(__dirname, 'cli.original.cjs');
const BACKUP = TARGET + '.bak';
// ─── Regex-based patches (version-agnostic) ──────────────
const patches = [
{
name: 'USER_TYPE → ant',
pattern: /function ([\w$]+)\(\)\{return"external"\}/g,
replacer: (m, fn) => `function ${fn}(){return"ant"}`,
sentinel: 'return"external"',
},
{
name: 'GrowthBook env overrides',
pattern: /function ([\w$]+)\(\)\{if\(!([\w$]+)\)\2=!0;return ([\w$]+)\}/g,
replacer: (m, fn, flag, val) =>
`function ${fn}(){if(!${flag}){${flag}=!0;try{let e=process.env.CLAUDE_INTERNAL_FC_OVERRIDES;if(e)${val}=JSON.parse(e)}catch(e){}}return ${val}}`,
unique: true, // must match exactly 1
},
{
name: 'GrowthBook config overrides',
pattern: /function ([\w$]+)\(\)\{return\}(function)/g,
replacer: (m, fn, next) =>
`function ${fn}(){try{return j8().growthBookOverrides??null}catch{return null}}${next}`,
selectIndex: 0, // first match only (there may be others)
validate: (match, code) => {
// Must be near other GrowthBook functions
const pos = code.indexOf(match);
const nearby = code.substring(Math.max(0, pos - 500), pos + 500);
return nearby.includes('growthBook') || nearby.includes('GrowthBook') || nearby.includes('FeatureValue');
},
},
{
name: 'Agent Teams always enabled',
pattern: /function ([\w$]+)\(\)\{if\(![\w$]+\(process\.env\.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\)&&![\w$]+\(\)\)return!1;if\(![\w$]+\("tengu_amber_flint",!0\)\)return!1;return!0\}/g,
replacer: (m, fn) => `function ${fn}(){return!0}`,
},
{
name: 'Computer Use subscription bypass',
pattern: /function ([\w$]+)\(\)\{let [\w$]+=[\w$]+\(\);return [\w$]+==="max"\|\|[\w$]+==="pro"\}/g,
replacer: (m, fn) => `function ${fn}(){return!0}`,
},
{
name: 'Computer Use default enabled',
pattern: /([\w$]+=)\{enabled:!1,pixelValidation/g,
replacer: (m, prefix) => `${prefix}{enabled:!0,pixelValidation`,
},
{
name: 'Ultraplan enable',
pattern: /(name:"ultraplan",description:`[^`]+`,argumentHint:"<prompt>",isEnabled:\(\)=>)!1/g,
replacer: (m, prefix) => `${prefix}!0`,
optional: true, // v2.1.89+ merged into /plan, no standalone command
},
{
// ≤v2.1.110: function X(){return Y("tengu_review_bughunter_config",null)?.enabled===!0}
// v2.1.119+: function X(){return Y("tengu_review_bughunter_config",null)} — getter
// and the gate at function Z(){return X()?.enabled===!0} elsewhere.
// We override the getter to always return {enabled:!0}.
name: 'Ultrareview enable',
pattern: /function ([\w$]+)\(\)\{return [\w$]+\("tengu_review_bughunter_config",null\)(\?\.enabled===!0)?\}/g,
replacer: (m, fn) => `function ${fn}(){return{enabled:!0}}`,
sentinel: '"tengu_review_bughunter_config"',
},
{
name: 'Computer Use gate bypass',
pattern: /function ([\w$]+)\(\)\{return [\w$]+\(\)&&[\w$]+\(\)\.enabled\}/g,
replacer: (m, fn) => `function ${fn}(){return!0}`,
},
{
name: 'Voice Mode enable (bypass GrowthBook kill)',
pattern: /function ([\w$]+)\(\)\{return![\w$]+\("tengu_amber_quartz_disabled",!1\)\}/g,
replacer: (m, fn) => `function ${fn}(){return!0}`,
},
{
// ≤v2.1.110: let Y=Dq();if(Y!=="firstParty"&&...)return!1;return/^claude-(opus|sonnet)-4-6/.test(K)
// v2.1.119+: same gate plus extra branches for claude-opus-4-7. We drop the
// gate prefix; downstream model regexes still run.
name: 'Auto-mode unlock for third-party API',
pattern: /let ([\w$]+)=[\w$]+\(\);if\(\1!=="firstParty"&&\1!=="anthropicAws"\)return!1;/g,
replacer: () => '',
sentinel: '!=="firstParty"&&',
},
// ── 绿色主题 (patch 标识) ──
{
name: 'Logo + brand color → green (RGB dark)',
pattern: /clawd_body:"rgb\(215,119,87\)"/g,
replacer: () => 'clawd_body:"rgb(34,197,94)"',
},
{
name: 'Logo + brand color → green (ANSI)',
pattern: /clawd_body:"ansi:redBright"/g,
replacer: () => 'clawd_body:"ansi:greenBright"',
},
{
name: 'Theme claude color → green (dark)',
pattern: /claude:"rgb\(215,119,87\)"/g,
replacer: () => 'claude:"rgb(34,197,94)"',
},
{
name: 'Theme claude color → green (light)',
pattern: /claude:"rgb\(255,153,51\)"/g,
replacer: () => 'claude:"rgb(22,163,74)"',
},
{
name: 'Shimmer → green',
pattern: /claudeShimmer:"rgb\(2[34]5,1[45]9,1[12]7\)"/g,
replacer: () => 'claudeShimmer:"rgb(74,222,128)"',
},
{
name: 'Shimmer light → green',
pattern: /claudeShimmer:"rgb\(255,183,101\)"/g,
replacer: () => 'claudeShimmer:"rgb(34,197,94)"',
},
{
name: 'Hex brand color → green',
pattern: /#da7756/g,
replacer: () => '#22c55e',
},
// ── 限制移除 ──
{
name: 'Remove CYBER_RISK_INSTRUCTION',
pattern: /([\w$]+)="IMPORTANT: Assist with authorized security testing[^"]*"/g,
replacer: (m, varName) => `${varName}=""`,
sentinel: 'Assist with authorized security testing',
},
{
name: 'Remove URL generation restriction',
pattern: /\n\$\{[\w$]+\}\nIMPORTANT: You must NEVER generate or guess URLs[^.]*\. You may use URLs provided by the user in their messages or local files\./g,
replacer: () => '',
sentinel: 'IMPORTANT: You must NEVER generate or guess URLs',
},
{
name: 'Remove cautious actions section',
pattern: /function ([\w$]+)\(\)\{return`# Executing actions with care\n\n[\s\S]*?`\}/g,
replacer: (m, fn) => `function ${fn}(){return\`\`}`,
sentinel: '# Executing actions with care',
},
{
name: 'Remove "Not logged in" notice',
pattern: /Not logged in\. Run [\w ]+ to authenticate\./g,
replacer: () => '',
optional: true,
},
// ── 消息过滤 ──
{
// v2.1.88-~v2.1.91: fn()!=="ant"){if(q.attachment.type==="hook_additional_context"...
// v2.1.92+ : fn()!=="ant"&&paY.has(q.attachment.type) — paY is an empty Set
// in v2.1.110, so this filter is effectively a no-op; patch anyway
// to guard against paY being populated in future versions.
name: 'Attachment filter bypass',
pattern: /([\w$]+)\(\)!=="ant"(&&[\w$]+\.has\([\w$]+\.attachment\.type\)|\)\{if\([\w$]+\.attachment\.type==="hook_additional_context")/g,
replacer: (m) => m.replace(/([\w$]+)\(\)!=="ant"/, 'false'),
optional: true, // filter may be removed entirely in future versions
},
{
// Legacy (≤v2.1.91) ternary form: fn()!=="ant"?tRY(_,sRY(K)):K
name: 'Message list filter bypass (legacy ternary)',
pattern: /([\w$]+)\(\)!=="ant"\?([\w$]+)\(([\w$]+),([\w$]+)\(([\w$]+)\)\):([\w$]+)/g,
replacer: (m, fn, tRY, underscore, sRY, K, fallback) => fallback,
optional: true, // removed in v2.1.92+
},
{
// v2.1.92+ (s_8): if(fn()==="ant")return _;let z=...;return FaY(_,z)
// Flip the guard so non-ant users also return the pre-filtered list.
name: 'Message list filter bypass (s_8 form)',
pattern: /if\(([\w$]+)\(\)==="ant"\)return ([\w$]+);let ([\w$]+)=([\w$]+) instanceof Set\?\4:([\w$]+)\(\4\);return ([\w$]+)\(\2,\3\)/g,
replacer: (m, fn, ret) => `return ${ret}`,
optional: true, // legacy versions had a ternary instead
},
];
// ─── Main ─────────────────────────────────────────────────
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const verify = args.includes('--verify');
const revert = args.includes('--revert');
if (revert) {
if (!existsSync(BACKUP)) { console.error('❌ No backup found'); process.exit(1); }
copyFileSync(BACKUP, TARGET);
console.log('✅ Reverted from backup');
process.exit(0);
}
if (!existsSync(TARGET)) {
console.error('❌ Target not found:', TARGET);
process.exit(1);
}
let code = readFileSync(TARGET, 'utf8');
const origSize = code.length;
// Extract version
const verMatch = code.match(/Version:\s*([\d.]+)/);
const version = verMatch ? verMatch[1] : 'unknown';
console.log(`\n${'═'.repeat(55)}`);
console.log(` ClawGod (universal)`);
console.log(` Target: cli.original.cjs (v${version})`);
console.log(` Mode: ${dryRun ? 'DRY RUN' : verify ? 'VERIFY' : 'APPLY'}`);
console.log(`${'═'.repeat(55)}\n`);
let applied = 0, skipped = 0, failed = 0;
for (const p of patches) {
const matches = [...code.matchAll(p.pattern)];
let relevant = matches;
// Filter by validation if provided
if (p.validate) {
relevant = matches.filter(m => p.validate(m[0], code));
}
// Select specific match index
if (p.selectIndex !== undefined) {
relevant = relevant.length > p.selectIndex ? [relevant[p.selectIndex]] : [];
}
// Uniqueness check — skip when 0 so the sentinel / already-applied
// fallthrough can handle it; only fail on >1 (ambiguous).
if (p.unique && relevant.length > 1) {
console.log(` ⚠️ ${p.name} — ${relevant.length} matches, skipping (need 1)`);
failed++;
continue;
}
if (relevant.length === 0) {
if (p.optional) {
console.log(` ⏭ ${p.name} (not present in this version)`);
skipped++;
continue;
}
// If the patch declares a sentinel (a string that must NOT exist in a
// fully-patched file), use it to tell "already applied" apart from
// "regex is stale and silently missed the target".
if (p.sentinel !== undefined) {
const sentinels = Array.isArray(p.sentinel) ? p.sentinel : [p.sentinel];
const stillPresent = sentinels.filter((s) => code.includes(s));
if (stillPresent.length > 0) {
console.log(` ❌ ${p.name} — regex stale, sentinel still in source: ${stillPresent.map((s) => JSON.stringify(s)).join(', ')}`);
failed++;
continue;
}
console.log(` ✅ ${p.name} (already applied, sentinel absent)`);
applied++;
continue;
}
console.log(` ⚠️ ${p.name} (0 matches, no sentinel — cannot verify)`);
skipped++;
continue;
}
if (verify) {
console.log(` ⬚ ${p.name} — ${relevant.length} match(es), not yet applied`);
skipped++;
continue;
}
// Apply patch
let count = 0;
for (const m of relevant) {
const replacement = p.replacer(m[0], ...m.slice(1));
if (replacement !== m[0]) {
if (!dryRun) {
code = code.replace(m[0], replacement);
}
count++;
}
}
if (count > 0) {
console.log(` ✅ ${p.name} (${count} replacement${count > 1 ? 's' : ''})`);
applied++;
} else {
console.log(` ⏭ ${p.name} (no change needed)`);
skipped++;
}
}
console.log(`\n${'─'.repeat(55)}`);
console.log(` Result: ${applied} applied, ${skipped} skipped, ${failed} failed`);
if (!dryRun && !verify && applied > 0) {
if (!existsSync(BACKUP)) {
copyFileSync(TARGET, BACKUP);
console.log(` 📦 Backup: ${BACKUP}`);
}
writeFileSync(TARGET, code, 'utf8');
const diff = code.length - origSize;
console.log(` 📝 Written: cli.original.cjs (${diff >= 0 ? '+' : ''}${diff} bytes)`);
}
console.log(`${'═'.repeat(55)}\n`);
PATCHER_EOF
info "Patcher created (patch.mjs)"
# ─── Apply patches ─────────────────────────────────────
dim "Applying patches ..."
node "$CLAWGOD_DIR/patch.mjs" 2>&1 | while IFS= read -r line; do echo " $line"; done
# ─── Create default configs ───────────────────────────
if [ ! -f "$CLAWGOD_DIR/features.json" ]; then
cat > "$CLAWGOD_DIR/features.json" << 'FEATURES_EOF'
{
"tengu_harbor": true,
"tengu_session_memory": true,
"tengu_amber_flint": true,
"tengu_auto_background_agents": true,
"tengu_destructive_command_warning": true,
"tengu_immediate_model_command": true,
"tengu_desktop_upsell": false,
"tengu_malort_pedway": {"enabled": true},
"tengu_amber_quartz_disabled": false,
"tengu_prompt_cache_1h_config": {"allowlist": ["*"]}
}
FEATURES_EOF
info "Default features.json created"
fi
# ─── Replace claude command ───────────────────────────
LAUNCHER_CONTENT="#!/bin/bash
# clawgod launcher
CLAWGOD_CLI=\"$CLAWGOD_DIR/cli.cjs\"
BUN_BIN=\"$BUN_BIN\"
if [ ! -f \"\$CLAWGOD_CLI\" ]; then
echo \"clawgod: installation at $CLAWGOD_DIR is missing (cli.cjs not found)\" >&2
echo \"clawgod: reinstall via curl -fsSL https://github.com/0Chencc/clawgod/releases/latest/download/install.sh | bash\" >&2
echo \"clawgod: or remove this launcher: rm \\\"\$0\\\"\" >&2
exit 127
fi
if [ ! -x \"\$BUN_BIN\" ]; then
if command -v bun >/dev/null 2>&1; then BUN_BIN=\"\$(command -v bun)\"; fi
fi
if [ ! -x \"\$BUN_BIN\" ]; then
echo \"clawgod: bun runtime not found at \$BUN_BIN\" >&2
echo \"clawgod: install bun curl -fsSL https://bun.sh/install | bash\" >&2
exit 127
fi
exec \"\$BUN_BIN\" \"\$CLAWGOD_CLI\" \"\$@\""
# Detect where claude is actually installed (supports native, npm, pnpm, yarn)
CLAUDE_BIN=$(which claude 2>/dev/null)
if [ -z "$CLAUDE_BIN" ]; then
# No claude in PATH — use default location
CLAUDE_BIN="$BIN_DIR/claude"
dim "No existing claude found, installing to $BIN_DIR"
fi
CLAUDE_DIR=$(dirname "$CLAUDE_BIN")
# Back up original claude (only once)
if [ ! -e "$CLAUDE_BIN.orig" ]; then
if [ -L "$CLAUDE_BIN" ]; then
# Symlink (native install) — preserve target
NATIVE_BIN="$(readlink "$CLAUDE_BIN")"
ln -sf "$NATIVE_BIN" "$CLAUDE_BIN.orig"
info "Original claude backed up → claude.orig (→ $NATIVE_BIN)"
elif [ -f "$CLAUDE_BIN" ] && file "$CLAUDE_BIN" 2>/dev/null | grep -q "Mach-O\|ELF\|script"; then
# Binary or script (pnpm/npm global install)
cp "$CLAUDE_BIN" "$CLAUDE_BIN.orig"
info "Original claude backed up → claude.orig"
else
# Try versions dir as fallback
VERSIONS_DIR="$HOME/.local/share/claude/versions"
if [ -d "$VERSIONS_DIR" ]; then
NATIVE_BIN="$(ls -t "$VERSIONS_DIR"/* 2>/dev/null | while read f; do
file "$f" 2>/dev/null | grep -q "Mach-O\|ELF" && echo "$f" && break
done)" || true
if [ -n "$NATIVE_BIN" ]; then
ln -sf "$NATIVE_BIN" "$CLAUDE_BIN.orig"
info "Original claude backed up → claude.orig (→ $NATIVE_BIN)"
fi
fi
fi
fi
# Write launcher to the SAME directory where claude was found.
# CRITICAL: `echo > $f` follows symlinks — if $CLAUDE_BIN is a symlink
# (e.g. official ~/.local/bin/claude → ~/.local/share/claude/versions/X)
# we'd write our launcher into the real binary and destroy it. Always
# remove the existing entry first so we write a fresh regular file.
write_launcher() {
local target="$1"
local dir
dir=$(dirname "$target")
mkdir -p "$dir"
rm -f "$target"
printf '%s\n' "$LAUNCHER_CONTENT" > "$target"
chmod +x "$target"
}
write_launcher "$CLAUDE_BIN"
info "Command 'claude' → patched ($CLAUDE_BIN)"
# Also install to ~/.local/bin if claude was elsewhere (ensures PATH consistency)
if [ "$CLAUDE_DIR" != "$BIN_DIR" ]; then
write_launcher "$BIN_DIR/claude"
dim "Also installed to $BIN_DIR/claude"
fi
# Always expose an unambiguous `clawgod` alias alongside the `claude` override.
# Useful when:
# - Windows .exe overshadows our .cmd (clawgod has no .exe competitor)
# - User wants explicit "patched" intent
# - User restored claude.orig via uninstall but still wants the patched one
write_launcher "$BIN_DIR/clawgod"
info "Command 'clawgod' → patched ($BIN_DIR/clawgod)"
# ─── Check PATH ───────────────────────────────────────
if ! echo "$PATH" | grep -q "$CLAUDE_DIR" && ! echo "$PATH" | grep -q "$BIN_DIR"; then
# Detect shell config file
case "$(basename "$SHELL")" in
zsh) SHELL_RC="$HOME/.zshrc" ;;
bash) SHELL_RC="$HOME/.bashrc" ;;
fish) SHELL_RC="$HOME/.config/fish/config.fish" ;;
*) SHELL_RC="$HOME/.profile" ;;
esac
echo ""
warn "$BIN_DIR is not in PATH. Run:"
dim " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> $SHELL_RC && source $SHELL_RC"
fi
# ─── Flush shell cache ────────────────────────────────
hash -r 2>/dev/null
# ─── Done ─────────────────────────────────────────────
echo ""
echo -e " ${BOLD}${GREEN}ClawGod installed!${NC}"
echo ""
dim " claude — Start patched Claude Code (green logo)"
dim " claude.orig — Run original unpatched Claude Code"
echo ""
warn " If 'claude' still runs the old version, restart your terminal or run: hash -r"
echo ""
dim " Config: ~/.clawgod/provider.json"
dim " Flags: ~/.clawgod/features.json"
echo ""