Files
clawgod/install.ps1
T
0chencc 349d3b1b21 fix: Windows patch not taking effect, clarify project nature
Windows fixes:
- Rename claude.exe → claude.orig.exe instead of delete (works even
  if exe is running, handles repeated installs safely)
- Fix package.json: merge "type":"module" instead of overwriting
  (was destroying npm dependencies)
- Fix launcher path: proper expansion for paths with spaces

README (EN/ZH/JP):
- Clarify this is NOT a third-party client, but a runtime patch
  on top of official Claude Code that works across versions
2026-04-02 13:32:37 +09:00

492 lines
18 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,
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
$ClawDir = Join-Path $env:USERPROFILE ".clawgod"
$BinDir = Join-Path $env:USERPROFILE ".local\bin"
# ─── Colors ───────────────────────────────────────────
function Write-OK($msg) { Write-Host " ✓ $msg" -ForegroundColor Green }
function Write-Err($msg) { Write-Host " ✗ $msg" -ForegroundColor Red }
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"
}
foreach ($f in @("cli.js","cli.original.js","cli.original.js.bak","patch.js","node_modules")) {
$p = Join-Path $ClawDir $f
if (Test-Path $p) { Remove-Item -Recurse -Force $p }
}
Write-OK "ClawGod uninstalled"
Write-Host ""
exit 0
}
# ─── Prerequisites ────────────────────────────────────
try { $null = Get-Command node -ErrorAction Stop }
catch {
Write-Err "Node.js is required (>= 18). 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
}
try { $null = Get-Command npm -ErrorAction Stop }
catch {
Write-Err "npm is required"
exit 1
}
# ─── Install Claude Code from npm ─────────────────────
New-Item -ItemType Directory -Force -Path $ClawDir | Out-Null
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
Write-Dim "Installing @anthropic-ai/claude-code@$Version ..."
npm install --prefix $ClawDir "@anthropic-ai/claude-code@$Version" --save-exact --no-fund --no-audit 2>$null | Out-Null
$installedVer = node -e "console.log(require('$($ClawDir -replace '\\','/')/node_modules/@anthropic-ai/claude-code/package.json').version)"
Write-OK "Claude Code v$installedVer downloaded"
# ─── Copy bundle ──────────────────────────────────────
$srcCli = Join-Path $ClawDir "node_modules\@anthropic-ai\claude-code\cli.js"
$dstCli = Join-Path $ClawDir "cli.original.js"
Copy-Item -Force $srcCli $dstCli
Write-OK "Bundle extracted (cli.original.js)"
# ─── Ensure ESM support ───────────────────────────────
$pkgJson = Join-Path $ClawDir "package.json"
$pkg = Get-Content $pkgJson -Raw | ConvertFrom-Json
if (-not $pkg.type) {
$pkg | Add-Member -NotePropertyName "type" -NotePropertyValue "module" -Force
$pkg | ConvertTo-Json -Depth 10 | Set-Content $pkgJson -Encoding UTF8
}
# ─── Write wrapper (cli.js) ───────────────────────────
@'
#!/usr/bin/env node
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const claudeDir = join(homedir(), '.claude');
const clawgodDir = join(homedir(), '.clawgod');
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');
}
if (config.apiKey) {
process.env.ANTHROPIC_API_KEY ??= config.apiKey;
}
if (config.baseURL && config.baseURL !== defaultConfig.baseURL) {
process.env.ANTHROPIC_BASE_URL ??= config.baseURL;
}
if (config.apiKey && config.model) {
process.env.ANTHROPIC_MODEL ??= config.model;
}
if (config.apiKey && config.smallModel) {
process.env.ANTHROPIC_SMALL_FAST_MODEL ??= config.smallModel;
}
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.CLAUDE_CONFIG_DIR ??= configDir;
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 {}
}
await import('./cli.original.js');
'@ | Set-Content (Join-Path $ClawDir "cli.js") -Encoding UTF8
Write-OK "Wrapper created (cli.js)"
# ─── Write universal patcher ──────────────────────────
# (Same Node.js patcher as bash version — extract from install.sh or inline)
$patcherUrl = "https://raw.githubusercontent.com/0Chencc/clawgod/main/patcher.mjs"
# Inline the patcher 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.js');
const BACKUP = TARGET + '.bak';
const patches = [
{
name: 'USER_TYPE → ant',
pattern: /function (\w+)\(\)\{return"external"\}/g,
replacer: (m, fn) => `function ${fn}(){return"ant"}`,
},
{
name: 'GrowthBook env overrides',
pattern: /function (\w+)\(\)\{if\(!(\w+)\)(\w+)=!0;return (\w+)\}/g,
replacer: (m, fn, flag, flag2, val) =>
`function ${fn}(){if(!${flag}){${flag2}=!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`,
},
{
name: 'Ultraplan enable',
pattern: /(name:"ultraplan",description:`[^`]+`,argumentHint:"<prompt>",isEnabled:\(\)=>)!1/g,
replacer: (m, prefix) => `${prefix}!0`,
optional: true,
},
{
name: 'Ultrareview enable',
pattern: /function (\w+)\(\)\{return \w+\("tengu_review_bughunter_config",null\)\?\.enabled===!0\}/g,
replacer: (m, fn) => `function ${fn}(){return!0}`,
},
{
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}=""`,
},
{
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: () => '',
},
{
name: 'Remove cautious actions section',
pattern: /function (\w+)\(\)\{return`# Executing actions with care\n\n[\s\S]*?`\}/g,
replacer: (m, fn) => `function ${fn}(){return\`\`}`,
},
{
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"\)\{if\(\w+\.attachment\.type==="hook_additional_context")/g,
replacer: (m, orig) => m.replace(/\w+\(\)!=="ant"/, 'false'),
},
{
name: 'Message list filter bypass',
pattern: /(\w+)\(\)!=="ant"\?(\w+)\((\w+),(\w+)\((\w+)\)\):(\w+)/g,
replacer: (m, fn, tRY, underscore, sRY, K, fallback) => fallback,
},
];
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.js (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`);
failed++; continue;
}
if (relevant.length === 0) {
if (p.optional) { console.log(` >> ${p.name} (not in this version)`); skipped++; }
else { console.log(` OK ${p.name} (already applied)`); applied++; }
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.js (${code.length - origSize} bytes)`);
}
console.log(`${'='.repeat(55)}\n`);
'@
Set-Content (Join-Path $ClawDir "patch.js") $patcherCode -Encoding UTF8
Write-OK "Patcher created (patch.js)"
# ─── Apply patches ────────────────────────────────────
Write-Dim "Applying patches ..."
node (Join-Path $ClawDir "patch.js")
# ─── Create default configs ───────────────────────────
$featuresFile = Join-Path $ClawDir "features.json"
if (-not (Test-Path $featuresFile)) {
@'
{
"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
}
'@ | Set-Content $featuresFile -Encoding UTF8
Write-OK "Default features.json created"
}
# ─── Replace claude command ───────────────────────────
$cliPath = (Join-Path $ClawDir "cli.js") -replace '\\', '\\'
$launcherContent = "@echo off`r`nnode `"$cliPath`" %*"
# 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
}
}
# Rename claude.exe so .cmd takes precedence
# (rename works even if the exe is running; delete does not)
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, rename to .old for cleanup
$oldExe = Join-Path $BinDir "claude.old.exe"
Rename-Item $claudeExe $oldExe -Force -ErrorAction SilentlyContinue
Write-OK "Moved claude.exe aside (.cmd now takes priority)"
}
}
# Write .cmd launcher for 'claude'
foreach ($cmd in @("claude")) {
$launcherContent | Set-Content (Join-Path $BinDir "$cmd.cmd") -Encoding ASCII
}
Write-OK "Command 'claude' → 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-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 ""