Files
clawgod/install.ps1
omar a5dd271588
Upstream Sync / sync (push) Successful in 13s
ci: drop release workflow, switch install URLs to raw/HEAD
Gitea has no /releases/latest/download/<file> shortcut (GitHub-only),
so the URL we wired up last commit served 404. Switch all user-facing
install commands to /raw/HEAD/install.{sh,ps1} -- the HEAD ref resolves
to the default branch (secure-main) without naming it, gives a stable
"always latest" URL, and survives a future branch rename.

With install no longer routed through release assets, the release
workflow becomes dead weight: secure-main is the curated branch (every
merge is manually reviewed), so every commit there is already vetted.
A separate tagged-release step duplicates that gate without adding new
information. Drop .gitea/workflows/release.yml; tag manually if you
ever want an immutable audit checkpoint.

The orphan v1.1.7-secure.1 tag + release is left in place as a
historical snapshot; harmless and can be deleted from the Gitea UI
later if desired.
2026-05-21 03:47:26 +03:00

1512 lines
58 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
ClawGod Installer for Windows
.DESCRIPTION
Downloads Claude Code from npm, applies feature unlock patches,
and replaces the 'claude' command with the patched version.
.EXAMPLE
irm clawgod.0chen.cc/install.ps1 | iex
# or
.\install.ps1
.\install.ps1 -Version 2.1.89
.\install.ps1 -Uninstall
#>
param(
[string]$Version = "latest",
[switch]$Uninstall
)
$ErrorActionPreference = "Stop"
$ClawDir = Join-Path $env:USERPROFILE ".clawgod"
$BinDir = Join-Path $env:USERPROFILE ".local\bin"
# --- Colors -------------------------------------------
function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Err($msg) { Write-Host " [X] $msg" -ForegroundColor Red }
function Write-Warn($msg) { Write-Host " ! $msg" -ForegroundColor Yellow }
function Write-Dim($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
Write-Host ""
Write-Host " ClawGod Installer" -ForegroundColor White -NoNewline
Write-Host " (Windows)" -ForegroundColor DarkGray
Write-Host ""
# --- Uninstall ----------------------------------------
if ($Uninstall) {
# Restore original claude
$claudeOrig = Join-Path $BinDir "claude.orig.cmd"
$claudeCmd = Join-Path $BinDir "claude.cmd"
if (Test-Path $claudeOrig) {
Move-Item -Force $claudeOrig $claudeCmd
Write-OK "Original claude restored"
}
# Also check for .exe backup
$claudeExeOrig = Join-Path $BinDir "claude.orig.exe"
$claudeExe = Join-Path $BinDir "claude.exe"
if (Test-Path $claudeExeOrig) {
Move-Item -Force $claudeExeOrig $claudeExe
Write-OK "Original claude.exe restored"
}
# Remove explicit clawgod alias
$clawgodCmd = Join-Path $BinDir "clawgod.cmd"
if (Test-Path $clawgodCmd) {
Remove-Item -Force $clawgodCmd
Write-OK "Removed clawgod alias"
}
foreach ($f in @("cli.js","cli.cjs","cli.original.js","cli.original.cjs","cli.original.js.bak","cli.original.cjs.bak","patch.js","patch.mjs","extract-natives.mjs","post-process.mjs","repatch.mjs",".source-version","node_modules","bun-runtime","vendor")) {
$p = Join-Path $ClawDir $f
if (Test-Path $p) { Remove-Item -Recurse -Force $p }
}
Write-OK "ClawGod uninstalled"
Write-Host ""
Write-Dim "Restart your terminal for changes to take effect."
Write-Host ""
exit 0
}
# --- Prerequisites ------------------------------------
try { $null = Get-Command node -ErrorAction Stop }
catch {
Write-Err "Node.js is required (>= 18) for the patcher. Install from https://nodejs.org"
exit 1
}
$nodeVer = [int](node -e "console.log(process.versions.node.split('.')[0])")
if ($nodeVer -lt 18) {
Write-Err "Node.js >= 18 required (found v$nodeVer)"
exit 1
}
# --- Ensure Bun (runtime that executes the patched cli.js) ------------
$BunBin = $null
try { $BunBin = (Get-Command bun -ErrorAction Stop).Source } catch {}
if (-not $BunBin) {
$homeBun = Join-Path $env:USERPROFILE ".bun\bin\bun.exe"
if (Test-Path $homeBun) { $BunBin = $homeBun }
}
if (-not $BunBin) {
Write-Dim "Installing Bun (required runtime for v2.1.113+ cli.js) ..."
try {
Invoke-Expression "$(Invoke-RestMethod https://bun.sh/install.ps1)" 2>$null | Out-Null
} catch {}
$BunBin = Join-Path $env:USERPROFILE ".bun\bin\bun.exe"
if (-not (Test-Path $BunBin)) {
Write-Err "Bun installation failed. Install manually: https://bun.sh/install"
exit 1
}
}
# Resolve bun.ps1 -> bun.exe. When Bun is installed via `npm install -g bun`,
# Get-Command returns a .ps1 wrapper script. A .cmd launcher cannot invoke .ps1
# directly -- Windows opens the file association dialog instead of executing it.
# Probe known install paths instead of parsing wrapper scripts.
if ($BunBin -and $BunBin -match '\.ps1$') {
$resolved = $null
$bunDir = Split-Path $BunBin
# 1. npm global: bun.ps1 sits next to node_modules/bun/bin/bun.exe
$cand = Join-Path $bunDir "node_modules\bun\bin\bun.exe"
if (Test-Path $cand) { $resolved = $cand }
# 2. bun.sh official install
if (-not $resolved) {
$cand = Join-Path $env:USERPROFILE ".bun\bin\bun.exe"
if (Test-Path $cand) { $resolved = $cand }
}
# 3. Scoop: shim exe lives in ~/scoop/shims/
if (-not $resolved) {
$cand = Join-Path $env:USERPROFILE "scoop\shims\bun.exe"
if (Test-Path $cand) { $resolved = $cand }
}
# 4. Chocolatey: typically in C:\ProgramData\chocolatey\bin\
if (-not $resolved) {
$chocoBin = Join-Path $env:ProgramData "chocolatey\bin\bun.exe"
if (Test-Path $chocoBin) { $resolved = $chocoBin }
}
if ($resolved) {
Write-Dim "Resolved bun.ps1 -> $resolved"
$BunBin = $resolved
} else {
Write-Warn "Bun resolved to .ps1 wrapper ($BunBin). The launcher may not work."
Write-Warn "Consider installing Bun via bun.sh/install.ps1 for a native bun.exe."
}
}
Write-OK "Bun: $(& $BunBin --version)"
# --- Bun version pre-flight -------------------------------------------
# Anthropic builds the native binary with Bun's canary channel; stable
# bun.sh trails by one version. Bun < 1.3.14 panics on cli.original.cjs
# with "Expected CommonJS module to have a function wrapper". Refuse
# early -- no npm download / no patch / no late sanity surprise where
# PowerShell's NativeCommandError display buries the friendly message.
# Bump $MinBunVersion when Anthropic moves the embedded Bun forward
# again.
$MinBunVersion = '1.3.14'
$BunVersionRaw = ''
try {
$bunOut = & $BunBin --version 2>$null | Select-Object -First 1
if ($bunOut) { $BunVersionRaw = "$bunOut".Trim() }
} catch {}
$BunVersionNum = ($BunVersionRaw -split '-')[0]
$BunVersionOk = $false
try {
if ($BunVersionNum) {
$BunVersionOk = ([version]$BunVersionNum) -ge ([version]$MinBunVersion)
}
} catch {}
if (-not $BunVersionOk) {
Write-Host ""
Write-Err "Bun $BunVersionRaw is below the required minimum ($MinBunVersion)."
Write-Err ""
Write-Err " Anthropic builds claude-code with Bun's canary channel. Older Bun"
Write-Err " panics on cli.original.cjs with 'Expected CommonJS module to have"
Write-Err " a function wrapper'. This is a hard requirement, not a warning."
Write-Err ""
Write-Err " Upgrade with one of:"
Write-Err " bun upgrade --canary"
Write-Err " powershell -c ""iex & {`$(irm https://bun.sh/install.ps1)} -Version canary"""
Write-Err ""
Write-Err " If your bun is from scoop (the binary is behind a shim and refuses"
Write-Err " to self-replace, so 'bun upgrade' silently hangs):"
Write-Err " scoop uninstall bun"
Write-Err " irm https://bun.sh/install.ps1 | iex"
Write-Err " bun upgrade --canary"
Write-Err ""
Write-Err " Then re-run this installer."
exit 1
}
# --- ripgrep prerequisite (search/grep tool) --------------------------
# Hard prerequisite -- without rg the Grep tool inside Claude Code fails.
try {
$rgPath = (Get-Command rg -ErrorAction Stop).Source
Write-OK "ripgrep: $rgPath"
}
catch {
Write-Err "ripgrep (rg) is required but not found in PATH."
Write-Err " Claude Code's Grep tool will not function without it."
Write-Err ""
Write-Err " Install: winget install BurntSushi.ripgrep.MSVC"
Write-Err " or: scoop install ripgrep"
Write-Err " or: choco install ripgrep"
Write-Err ""
Write-Err " Re-run this script after installing rg."
exit 1
}
# --- Locate native Bun binary (cli.js source) --------------------------
# Source: npm registry (@anthropic-ai/claude-code-win32-<arch>).
# Local binary detection is intentionally skipped -- see policy note below.
New-Item -ItemType Directory -Force -Path $ClawDir | Out-Null
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
$NativeBin = $null
$NativeBinLabel = $null
$NativeBinTmpDir = $null
# Detect platform suffix
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64" -or $env:PROCESSOR_ARCHITEW6432 -eq "ARM64") {
$arch = "arm64"
} else {
$arch = "x64"
}
$platformSuffix = "win32-$arch"
# Detection policy: ALWAYS pull from the npm registry @latest.
#
# Earlier versions of this script also probed local install directories
# (versions/, claude.orig, npm-global, bun-global) before falling back to
# the registry. Every one of those is a stale-source trap: clawgod patches
# out `claude update`, so users never re-run the underlying installers,
# and those directories freeze at whatever version was on disk the day
# clawgod was first installed. `claude update` (which is now redirected
# here) would re-detect the frozen binary forever -- never reaching the
# registry. See INCIDENT_LOG 2026-04-29 entry. The fix is to skip local
# detection entirely; the npm tarball is ~60-90 MB compressed, fetched
# once per upgrade.
# npm registry -- pull the platform tarball directly via Node.
# Avoids depending on `npm` and `tar` being on PATH (older Windows 10
# builds lack tar.exe; some PowerShell shims mangle `& npm`). Node is
# already a hard prerequisite for the patcher, so reuse it.
if (-not $NativeBin) {
$npmPkg = "@anthropic-ai/claude-code-$platformSuffix"
Write-Dim "Fetching $npmPkg@latest from npm registry ..."
$NativeBinTmpDir = Join-Path $env:TEMP "clawgod-binary-$([Guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Force -Path $NativeBinTmpDir | Out-Null
$fetchScript = Join-Path $NativeBinTmpDir "fetch.mjs"
$useNpmFetch = $false
$noProxy = $env:NO_PROXY
if ($env:HTTPS_PROXY -or $env:HTTP_PROXY) {
if ($noProxy -match '(?i)npmjs\.org') {
Write-Dim "NO_PROXY includes npmjs.org -- using direct fetch"
} elseif (Get-Command npm -ErrorAction SilentlyContinue) {
$useNpmFetch = $true
} else {
Write-Warn "HTTP proxy detected but npm not found. fetch.mjs may not work through your proxy."
Write-Warn "Install npm or set NO_PROXY=registry.npmjs.org to bypass."
}
}
if ($useNpmFetch) {
Push-Location $NativeBinTmpDir
try {
$npmOut = npm pack "$npmPkg@latest" --silent 2>&1
$tarball = Get-ChildItem $NativeBinTmpDir -Filter "*.tgz" | Select-Object -First 1
if ($tarball) {
tar xzf $tarball.FullName 2>$null
$cand = Join-Path $NativeBinTmpDir "package\claude.exe"
if ((Test-Path $cand) -and (Get-Item $cand).Length -gt 10MB) {
$NativeBin = $cand
$pkgJson = Join-Path $NativeBinTmpDir "package\package.json"
if (Test-Path $pkgJson) {
$NativeBinLabel = (Get-Content $pkgJson -Raw | ConvertFrom-Json).version
} else { $NativeBinLabel = "npm-latest" }
Write-OK "Downloaded $npmPkg@$NativeBinLabel (via npm)"
}
}
} finally { Pop-Location }
if (-not $NativeBin) {
Remove-Item -Recurse -Force $NativeBinTmpDir -ErrorAction SilentlyContinue
Write-Err "npm pack failed. Output:"
Write-Dim ($npmOut -join "`n")
exit 1
}
} else {
@'
// Download a scoped npm tarball (no npm CLI dependency) and extract it
// using Node's built-in zlib + a minimal POSIX tar parser.
import { request as httpsRequest } from 'node:https';
import { request as httpRequest } from 'node:http';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { gunzipSync } from 'node:zlib';
import { URL } from 'node:url';
const [, , pkgSpec, outDir] = process.argv;
const last = pkgSpec.lastIndexOf('@');
const pkg = last > 0 ? pkgSpec.slice(0, last) : pkgSpec;
const ver = last > 0 ? pkgSpec.slice(last + 1) : 'latest';
function get(url, redirects = 0) {
return new Promise((resolve, reject) => {
if (redirects > 5) return reject(new Error(`Too many redirects`));
const parsed = new URL(url);
const reqMod = parsed.protocol === 'https:' ? httpsRequest : httpRequest;
const opts = { method: 'GET', hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search };
reqMod(opts, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume();
return get(res.headers.location, redirects + 1).then(resolve, reject);
}
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
}
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks)));
res.on('error', reject);
}).on('error', reject).end();
});
}
const metaBuf = await get(`https://registry.npmjs.org/${pkg}/${ver}`);
const meta = JSON.parse(metaBuf.toString('utf8'));
console.log(`Resolved ${pkg}@${meta.version}`);
const tgz = await get(meta.dist.tarball);
console.log(`Downloaded ${(tgz.length / 1024 / 1024).toFixed(1)} MB`);
const buf = gunzipSync(tgz);
mkdirSync(outDir, { recursive: true });
let off = 0, files = 0;
while (off + 512 <= buf.length) {
const name = buf.slice(off, off + 100).toString('utf8').replace(/\0+$/, '');
if (!name) break;
const sizeOct = buf.slice(off + 124, off + 136).toString('utf8').replace(/[\0\s]+$/, '');
const size = parseInt(sizeOct, 8) || 0;
const typeflag = String.fromCharCode(buf[off + 156]);
off += 512;
if (typeflag === '0' || typeflag === '\0') {
const dest = join(outDir, name);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(dest, buf.slice(off, off + size));
files++;
}
off += Math.ceil(size / 512) * 512;
}
console.log(`Extracted ${files} files`);
console.log(`VERSION=${meta.version}`);
'@ | Set-Content $fetchScript -Encoding UTF8
$output = & node $fetchScript "$npmPkg@latest" $NativeBinTmpDir 2>&1
$exitCode = $LASTEXITCODE
$output | ForEach-Object { Write-Host " $_" }
Remove-Item -Force $fetchScript -ErrorAction SilentlyContinue
if ($exitCode -ne 0) {
Remove-Item -Recurse -Force $NativeBinTmpDir -ErrorAction SilentlyContinue
Write-Err "Fetch failed (node exit $exitCode). Install the official binary manually:"
Write-Err " irm https://claude.ai/install.ps1 | iex"
exit 1
}
$cand = Join-Path $NativeBinTmpDir "package\claude.exe"
if ((Test-Path $cand) -and (Get-Item $cand).Length -gt 10MB) {
$NativeBin = $cand
$verLine = $output | Where-Object { $_ -match '^VERSION=' } | Select-Object -First 1
if ($verLine) { $NativeBinLabel = ($verLine -replace '^VERSION=', '').Trim() }
else { $NativeBinLabel = "npm-latest" }
} else {
Remove-Item -Recurse -Force $NativeBinTmpDir -ErrorAction SilentlyContinue
Write-Err "Tarball downloaded but expected package\claude.exe was missing or too small."
Write-Err " Tempdir kept for inspection: $NativeBinTmpDir"
exit 1
}
Write-OK "Downloaded $npmPkg@$NativeBinLabel"
}
}
if (-not $NativeBin) {
Write-Err "Native Claude Code binary not found"
Write-Err "Install the official binary first:"
Write-Err " irm https://claude.ai/install.ps1 | iex"
Write-Err "Then re-run this script."
exit 1
}
# Always write the extractor (used for cli.js and/or .node modules)
$extractorPath = Join-Path $ClawDir "extract-natives.mjs"
@'
#!/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) -------------------------
// Two anchors: bunfs path (primary, Mach-O/ELF) and cli_after_main_complete
// (fallback, used when Windows PE builds omit the bunfs path string).
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) {
let fnStart = -1;
const pathOff = buf.indexOf(CLI_PATH_MARKER);
if (pathOff !== -1) {
const candidate = buf.indexOf(CLI_FN_MARKER, pathOff);
if (candidate !== -1 && candidate - pathOff <= 1024) fnStart = candidate;
}
if (fnStart === -1) {
const tailMark = buf.indexOf(CLI_TAIL_MARKER);
if (tailMark === -1) return null;
const candidate = buf.lastIndexOf(CLI_FN_MARKER, tailMark);
if (candidate === -1 || tailMark - candidate < 1024 * 1024) return null;
fnStart = candidate;
}
const tailFromFn = buf.indexOf(CLI_TAIL_MARKER, fnStart);
if (tailFromFn === -1) return null;
const ending = buf.indexOf(CLI_END_MARKER, tailFromFn);
if (ending === -1 || ending - tailFromFn > 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(` [OK] ${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();
'@ | Set-Content $extractorPath -Encoding UTF8
# --- Extract cli.js + native modules from Bun binary ----------
$VendorDir = Join-Path $ClawDir "vendor"
if (Test-Path $VendorDir) { Remove-Item -Recurse -Force $VendorDir }
New-Item -ItemType Directory -Force -Path $VendorDir | Out-Null
$dstCli = Join-Path $ClawDir "cli.original.js"
Write-Dim "Extracting cli.js from $NativeBinLabel ..."
& node $extractorPath $NativeBin $ClawDir --cli-js 2>&1 | ForEach-Object { Write-Host " $_" }
if (-not (Test-Path $dstCli)) {
Write-Err "Failed to extract cli.js from native binary"
exit 1
}
Write-Dim "Extracting native modules from $NativeBinLabel ..."
& node $extractorPath $NativeBin $VendorDir 2>&1 | ForEach-Object { Write-Host " $_" }
# Note: keep extractorPath around -- repatch.mjs uses it on version drift
# --- Post-process cli.js for Bun runtime ----------------------
Write-Dim "Rewriting bunfs paths and IIFE invocation ..."
$postProc = Join-Path $ClawDir "post-process.mjs"
@'
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');
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')}))`,
);
code = code.replace(
/[\w$]+\.fileURLToPath\("file:\/\/\/home\/runner\/work\/claude-cli-internal\/claude-cli-internal\/[^"]*"\)/g,
() => '__filename',
);
code = code.replace(/\}\)\s*$/, '})(exports, require, module, __filename, __dirname)');
writeFileSync(dst, code);
unlinkSync(src);
console.log(`cli.original.cjs: ${code.length} bytes`);
'@ | Set-Content $postProc -Encoding UTF8
& node $postProc 2>&1 | ForEach-Object { Write-Host " $_" }
if (-not (Test-Path (Join-Path $ClawDir "cli.original.cjs"))) {
Write-Err "Post-process failed"
exit 1
}
# Stamp source version so wrapper can detect drift on next launch
Set-Content -Path (Join-Path $ClawDir ".source-version") -Value $NativeBinLabel -Encoding ASCII
# If we pulled the binary from npm into a tmpdir, clean up -- extraction
# is done; drift detection only consults %USERPROFILE%\.local\share\claude\versions\.
if ($NativeBinTmpDir -and (Test-Path $NativeBinTmpDir)) {
Remove-Item -Recurse -Force $NativeBinTmpDir -ErrorAction SilentlyContinue
}
Write-OK "cli.original.cjs ready ($NativeBinLabel)"
# --- Write re-patch helper (used by wrapper on version drift) ---------
@'
#!/usr/bin/env bun
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)}`);
'@ | Set-Content (Join-Path $ClawDir "repatch.mjs") -Encoding UTF8
Write-OK "Re-patch helper installed (repatch.mjs)"
# --- Write wrapper (cli.cjs, runs under Bun) ------------------
@'
#!/usr/bin/env bun
const { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, renameSync } = require('fs');
const { join, basename } = require('path');
const { homedir } = require('os');
const { spawnSync } = require('child_process');
const clawgodDir = join(homedir(), '.clawgod');
// Note: drift detection removed -- see install.sh wrapper for full notes.
// `versions/` either doesn't exist (Windows) or doesn't grow on healthy
// clawgod installs (we patch out `claude update`), so the check could only
// retract a fresh install.ps1 / install.sh upgrade. `claude update` ->
// install.sh redirect is the single source of truth for version upgrades.
// One-time migration: earlier wrapper versions set CLAUDE_CONFIG_DIR=~/.clawgod,
// which made Claude Code read/write ~/.clawgod/.claude.json instead of the
// native ~/.claude.json (the file holding MCP config, project history, session
// index). Move it back transparently on first run after upgrade.
const nativeClaudeJson = join(homedir(), '.claude.json');
const strayClaudeJson = join(clawgodDir, '.claude.json');
if (existsSync(strayClaudeJson) && !existsSync(nativeClaudeJson)) {
try { renameSync(strayClaudeJson, nativeClaudeJson); } catch {}
}
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;
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;
}
// Third-party Anthropic-compatible proxies (DeepSeek / OneAPI / Bedrock /
// vLLM / etc.) don't share Anthropic's server-side handling of
// x-anthropic-billing-header. That header carries a per-request `cch` field
// which Anthropic's own server excludes from prompt-cache key calculation
// (via cacheScope:null), but third-party proxies fold into the prefix hash --
// so the cached prefix changes every request and cache hit rate drops to
// zero. Auto-disable the header whenever baseURL points away from Anthropic.
// Users can force re-enable with CLAUDE_CODE_ATTRIBUTION_HEADER=1 if needed.
if (config.baseURL && !/anthropic\.com/i.test(config.baseURL)) {
process.env.CLAUDE_CODE_ATTRIBUTION_HEADER ??= '0';
}
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';
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');
'@ | Set-Content (Join-Path $ClawDir "cli.cjs") -Encoding UTF8
Write-OK "Wrapper created (cli.cjs)"
# --- Write universal patcher --------------------------
# (Same Node.js patcher as bash version -- inline to avoid extra download)
$patcherCode = @'
#!/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';
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,
},
{
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,
validate: (match, code) => {
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`,
},
{
// v2.1.92+: name:"ultraplan",get description(){...},argumentHint:"<prompt>",isEnabled:()=>fnRef()
// Older : name:"ultraplan",description:`...`,argumentHint:"<prompt>",isEnabled:()=>!1
name: 'Ultraplan enable',
pattern: /(name:"ultraplan",[\s\S]{1,500}?argumentHint:"<prompt>",isEnabled:\(\)=>)(?:!1|[\w$]+\(\))/g,
replacer: (m, prefix) => `${prefix}!0`,
sentinel: 'name:"ultraplan"',
},
{
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: '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: '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"&&Y!=="anthropicAws")return!1;return/^claude-(opus|sonnet)-4-6/.test(K)
// v2.1.119+: same gate plus extra branches for claude-opus-4-7.
// v2.1.139+: gate moved inside function wuH(H){let $=R7(H),q=Wq();if(q!=="firstParty"&&q!=="anthropicAws")return!1;if($.includes("claude-3-")||...)return!0;return!1}
// i.e. the `let` lifted to a comma-list before the if; the if-gate
// itself is unchanged shape. We drop only the if-gate; downstream
// model allow-list still runs and now accepts third-party calls.
name: 'Auto-mode unlock for third-party API',
pattern: /if\(([\w$]+)!=="firstParty"&&\1!=="anthropicAws"\)return!1;/g,
replacer: () => '',
sentinel: '!=="firstParty"&&',
},
{
// Redirect CLI `claude update` to clawgod self-update. Upstream's
// detectInstallType() returns "unknown" under our launcher; the
// unknown-fallback either silently downgrades ~/.bun/bin/bun (macOS) or
// writes the new binary outside our drift-detection scan path (Windows).
// Our redirect funnels the upgrade through install.{sh,ps1} so the new
// version is re-extracted, re-patched, and re-launchered without ever
// touching the bun runtime. Escape hatch for users who want vanilla
// update is printed every run.
name: "Redirect `claude update` to clawgod self-update",
pattern: /(\.command\("update"\)\.alias\("upgrade"\)\.description\("[^"]+"\)\.action\(async\(\)=>\{)/g,
replacer: (m, prefix) => {
// PowerShell 5.1's Invoke-WebRequest ignores HTTP_PROXY/HTTPS_PROXY env
// (only reads IE system proxy). Read env explicitly and pass via -Proxy
// so it works on both PS 5.1 and PS 7. Use Invoke-RestMethod (irm) not
// Invoke-WebRequest (iwr): under -UseBasicParsing on PS 5.1, iwr's
// .Content is byte[] not string, so `iex (iwr -useb ...).Content`
// throws "Cannot convert System.Byte[] to System.String". irm always
// returns string in both versions. -EncodedCommand bypasses CLI
// arg-quoting; payload must be UTF-16LE base64.
const psScript =
"$p=if($env:HTTPS_PROXY){$env:HTTPS_PROXY}elseif($env:HTTP_PROXY){$env:HTTP_PROXY}else{$null};" +
"$u='https://git.qomar.pw/omar/clawgod/raw/HEAD/install.ps1';" +
"if($p){iex(irm -Proxy $p $u)}else{iex(irm $u)}";
const psB64 = Buffer.from(psScript, 'utf16le').toString('base64');
return (
prefix +
`process.stderr.write("[clawgod] 'claude update' is handled by clawgod self-update.\\n[clawgod] To leave clawgod and use vanilla update: bash ~/.clawgod/install.sh --uninstall\\n[clawgod] Continuing now\\u2026\\n");` +
`const _w=process.platform==='win32';` +
`const _c=_w?['powershell','-NoProfile','-EncodedCommand','${psB64}']:['bash','-c','curl -fsSL https://git.qomar.pw/omar/clawgod/raw/HEAD/install.sh | bash'];` +
`const _r=require('child_process').spawnSync(_c[0],_c.slice(1),{stdio:'inherit'});` +
`process.exit(_r.status||0);`
);
},
sentinel: '.command("update").alias("upgrade")',
},
{
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',
// v2.1.88-~v2.1.122: function GSY(){return`# Executing actions...`}
// v2.1.123+: function _j3(H){if(LE8(H)==="compact")return`# Executing...short`;return`# Executing...long`}
pattern: /function ([\w$]+)\(([\w$]*)\)\{(?:if\([\s\S]{1,200}?\)return`# Executing actions with care\n\n[\s\S]*?`;)?return`# Executing actions with care\n\n[\s\S]*?`\}/g,
replacer: (m, fn, arg) => `function ${fn}(${arg}){return\`\`}`,
sentinel: '# Executing actions with care',
},
{
name: 'Remove "Not logged in" notice',
pattern: /Not logged in\. Run [\w ]+ to authenticate\./g,
replacer: () => '',
optional: true,
},
{
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,
},
{
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,
},
{
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,
},
];
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;
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;
if (p.validate) relevant = matches.filter(m => p.validate(m[0], code));
if (p.selectIndex !== undefined) relevant = relevant.length > p.selectIndex ? [relevant[p.selectIndex]] : [];
if (p.unique && relevant.length > 1) {
console.log(` ?? ${p.name} -- ${relevant.length} matches (need 1)`);
failed++; continue;
}
if (relevant.length === 0) {
if (p.optional) { console.log(` >> ${p.name} (not in this version)`); skipped++; continue; }
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(` XX ${p.name} -- regex stale, sentinel still present: ${stillPresent.map((s) => JSON.stringify(s)).join(', ')}`);
failed++; continue;
}
console.log(` OK ${p.name} (already applied, sentinel absent)`); applied++; continue;
}
console.log(` !! ${p.name} (0 matches, no sentinel)`); skipped++;
continue;
}
if (verify) { console.log(` -- ${p.name} -- not yet applied`); skipped++; continue; }
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(` OK ${p.name} (${count})`); applied++; }
else { console.log(` >> ${p.name} (no change)`); 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');
console.log(` Written: cli.original.cjs (${code.length - origSize} bytes)`);
}
console.log(`${'='.repeat(55)}\n`);
'@
Set-Content (Join-Path $ClawDir "patch.mjs") $patcherCode -Encoding UTF8
Write-OK "Patcher created (patch.mjs)"
# --- Apply patches ------------------------------------
Write-Dim "Applying patches ..."
node (Join-Path $ClawDir "patch.mjs")
# --- Create default configs ---------------------------
$featuresFile = Join-Path $ClawDir "features.json"
if (-not (Test-Path $featuresFile)) {
$featuresJson = @'
{
"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": ["*"]}
}
'@
[System.IO.File]::WriteAllText($featuresFile, $featuresJson, (New-Object System.Text.UTF8Encoding $false))
Write-OK "Default features.json created"
}
# --- Sanity check: ensure user's Bun can actually load cli.original.cjs --
# Anthropic builds the native binary with a bleeding-edge Bun build (e.g.
# 1.3.14 while stable still ships 1.3.13). Older Bun crashes loading the
# extracted cli.original.cjs with "Expected CommonJS module to have a
# function wrapper". Detect this BEFORE we install the launcher -- better
# to fail loudly than to leave the user with a launcher that panics on
# first invocation.
Write-Dim "Verifying Bun can load patched cli.original.cjs ..."
$sanityCli = Join-Path $ClawDir "cli.cjs"
# PowerShell folds native-command stderr into the error stream as
# ErrorRecord objects; with $ErrorActionPreference='Stop' (common when
# this script is piped through `iex`) that terminates BEFORE we even
# read $sanityOut. Localize ErrorActionPreference + try/catch so the
# panic message reliably lands in $sanityOut and our friendly Write-Err
# block runs. Defense-in-depth -- pre-flight already blocks Bun < $MinBunVersion;
# this remains for the day Anthropic bumps embedded Bun past our constant.
$sanityOut = $null
try {
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
$sanityOut = (& $BunBin $sanityCli --version 2>&1 | Out-String)
} catch {
$sanityOut = "$_"
} finally {
$ErrorActionPreference = $prevEAP
}
if ($sanityOut -match "Expected CommonJS module to have a function wrapper") {
Write-Host ""
Write-Err "Bun $(& $BunBin --version) cannot load Anthropic's cli.original.cjs."
Write-Err ""
Write-Err " Anthropic builds with Bun's canary channel (currently ~1.3.14), while"
Write-Err " bun.sh's main download is on stable (currently 1.3.13). The canary build"
Write-Err " is NOT visible on bun.sh's download page -- it lives on GitHub Releases"
Write-Err " and is reachable only via 'bun upgrade --canary'."
Write-Err ""
Write-Err " If your bun is from bun.sh:"
Write-Err " bun upgrade --canary"
Write-Err " or: powershell -c ""iex & {`$(irm https://bun.sh/install.ps1)} -Version canary"""
Write-Err ""
Write-Err " If your bun is from scoop (the binary is behind a shim and refuses to"
Write-Err " self-replace, so 'bun upgrade' silently hangs):"
Write-Err " scoop uninstall bun"
Write-Err " irm https://bun.sh/install.ps1 | iex"
Write-Err " bun upgrade --canary"
Write-Err ""
Write-Err " Then re-run .\install.ps1 -- this sanity check will pass."
exit 1
}
Write-OK "Bun loads cli.original.cjs"
# --- Replace claude command ---------------------------
# Build launcher content using %USERPROFILE% env var where possible to avoid
# encoding issues when the profile path contains non-ASCII characters (e.g.
# Chinese/Korean/Japanese usernames). cmd.exe resolves %USERPROFILE% at
# runtime so no problematic characters need to be baked into the .cmd file.
$cliPathInCmd = "%USERPROFILE%\.clawgod\cli.cjs"
$normalizedUserProfile = $env:USERPROFILE.TrimEnd('\', '/')
$normalizedBunBin = $BunBin.TrimEnd('\', '/')
$userProfilePrefix = "$normalizedUserProfile\"
if ($normalizedBunBin.Equals($normalizedUserProfile, [StringComparison]::OrdinalIgnoreCase) -or
$normalizedBunBin.StartsWith($userProfilePrefix, [StringComparison]::OrdinalIgnoreCase)) {
$bunRelative = $normalizedBunBin.Substring($normalizedUserProfile.Length).TrimStart('\', '/')
$bunPathInCmd = "%USERPROFILE%\$bunRelative"
} else {
# Bun outside USERPROFILE (e.g. system-wide install) -- fall back to
# absolute path since %USERPROFILE%-relative expansion doesn't apply.
$bunPathInCmd = $BunBin
}
$launcherContent = "@echo off`r`nif not exist `"$cliPathInCmd`" (`r`n echo clawgod: cli.cjs not found. Reinstall: irm https://git.qomar.pw/omar/clawgod/raw/HEAD/install.ps1 ^| iex`r`n exit /b 127`r`n)`r`nif not exist `"$bunPathInCmd`" (`r`n echo clawgod: bun not found at $bunPathInCmd. Install: https://bun.sh/install`r`n exit /b 127`r`n)`r`n`"$bunPathInCmd`" `"$cliPathInCmd`" %*"
# Find and back up original claude
$claudeCmd = Join-Path $BinDir "claude.cmd"
$claudeExe = Join-Path $BinDir "claude.exe"
$claudeOrigCmd = Join-Path $BinDir "claude.orig.cmd"
$claudeOrigExe = Join-Path $BinDir "claude.orig.exe"
# Check multiple locations for original claude
$originalFound = $false
foreach ($loc in @(
(Join-Path $BinDir "claude.exe"),
(Join-Path $BinDir "claude.cmd"),
(Join-Path $env:USERPROFILE ".local\share\claude\versions"),
(Join-Path $env:LOCALAPPDATA "Programs\claude-code")
)) {
if (Test-Path $loc) {
# Back up .exe if exists and not already backed up
if ($loc -like "*.exe" -and -not (Test-Path $claudeOrigExe)) {
Copy-Item $loc $claudeOrigExe -Force
Write-OK "Original claude.exe backed up -> claude.orig.exe"
$originalFound = $true
}
# Back up .cmd if exists and not already backed up
if ($loc -like "*.cmd" -and -not (Test-Path $claudeOrigCmd)) {
Copy-Item $loc $claudeOrigCmd -Force
Write-OK "Original claude.cmd backed up -> claude.orig.cmd"
$originalFound = $true
}
# If it's a versions directory, find the latest exe
if (Test-Path $loc -PathType Container) {
$latestExe = Get-ChildItem $loc -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latestExe -and -not (Test-Path $claudeOrigExe)) {
Copy-Item $latestExe.FullName $claudeOrigExe -Force
Write-OK "Original claude backed up -> claude.orig.exe ($($latestExe.Name))"
$originalFound = $true
}
}
break
}
}
# Clean up leftover timestamped/old exes from previous installs
Get-ChildItem $BinDir -Filter "claude.*.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ne "claude.orig.exe" } |
ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue }
# Remove claude.exe so .cmd takes precedence
# Keep one backup as claude.orig.exe, discard the rest
if (Test-Path $claudeExe) {
if (-not (Test-Path $claudeOrigExe)) {
Rename-Item $claudeExe $claudeOrigExe -Force
Write-OK "Renamed claude.exe -> claude.orig.exe"
} else {
# Backup already exists -- just remove the new claude.exe
try {
Remove-Item -Force $claudeExe
} catch {
# File locked (running process) -- rename aside with timestamp
$ts = Get-Date -Format "yyyyMMddHHmmss"
Rename-Item $claudeExe "claude.$ts.exe" -Force -ErrorAction SilentlyContinue
}
Write-OK "Removed claude.exe (.cmd now takes priority)"
}
}
# Write .cmd launcher for both 'claude' and the explicit 'clawgod' alias.
# Why both:
# - claude.cmd may be shadowed by a claude.exe higher in PATH
# - clawgod.cmd has no .exe competitor, so it always works
# - User can invoke patched explicitly via `clawgod` regardless of which
# binary 'claude' resolves to
foreach ($cmd in @("claude", "clawgod")) {
$launcherContent | Set-Content (Join-Path $BinDir "$cmd.cmd") -Encoding Default
}
Write-OK "Commands 'claude' + 'clawgod' -> patched"
# --- Ensure BinDir is in PATH -------------------------
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$BinDir*") {
[Environment]::SetEnvironmentVariable("Path", "$BinDir;$userPath", "User")
$env:Path = "$BinDir;$env:Path"
Write-OK "Added $BinDir to user PATH"
Write-Dim "(restart terminal for PATH to take effect)"
}
# --- Done ---------------------------------------------
Write-Host ""
Write-Host " ClawGod installed!" -ForegroundColor Green
Write-Host ""
Write-Dim " claude -- Start patched Claude Code (green logo)"
Write-Dim " claude.orig -- Run original unpatched Claude Code"
Write-Host ""
Write-Dim " Updates: 'claude update' is patched to route through this installer."
Write-Dim " Just run it as usual -- pulls latest Anthropic release + re-patches"
Write-Dim " in one step. To leave clawgod and use vanilla update:"
Write-Dim " bash ~/.clawgod/install.sh --uninstall"
Write-Host ""
Write-Err " If 'claude' still runs the old version, restart your terminal."
Write-Host ""
Write-Dim " Config: ~/.clawgod/provider.json"
Write-Dim " Flags: ~/.clawgod/features.json"
Write-Host ""
Write-Dim " If 'claude' panics with 'Expected CommonJS module to have a function wrapper',"
Write-Dim " your Bun lags Anthropic's embedded Bun. Upgrade with one of:"
Write-Dim " bun upgrade --canary (if installed from bun.sh)"
Write-Dim " scoop update bun (scoop -- may lag stable)"
Write-Dim " irm https://bun.sh/install.ps1 | iex (re-install latest)"
Write-Host ""