Bezetacil Toolset — Electron Runtime Introspection
Pipeline ASAR, deobfuscation, harness runtime, debugger spy, CDP extraction
Bezetacil Toolset — Electron Runtime Introspection
Toolset for reverse engineering, patching, and runtime introspection of Electron apps (developed for Claude Desktop).
Overview
┌─────────────────────────────────────────────────────────┐
│ 1. ASAR Pipeline │ Extract → Patch → Repack → Sign │
│ 2. Deobfuscation │ webcrack → deloom → analysis │
│ 3. Bezetacil Harness │ Runtime injection + hot reload │
│ 4. Debugger Spy │ Network capture + WebSocket │
│ 5. CDP Tools │ Python PEP 723 scripts │
│ 6. Bridge Server │ Bidirectional TCP Python ↔ Node │
└─────────────────────────────────────────────────────────┘
1. ASAR Pipeline
Tools
# Install
npm install -g @electron/asar deloom webcrack
# Binary locations
~/.hermes/node/bin/asar
~/.hermes/node/bin/deloom
~/.hermes/node/bin/webcrack
Complete Pipeline
# 1. Backup
cp /Applications/Claude.app/Contents/Resources/app.asar /tmp/app.asar.backup
# 2. Extract (with symlink to unpacked)
ln -sf /Applications/Claude.app/Contents/Resources/app.asar.unpacked /tmp/app.asar.backup.unpacked
asar extract /tmp/app.asar.backup /tmp/extracted/
# 3. Patch (Python)
python3 -c "
with open('/tmp/extracted/.vite/build/index.js') as f: content = f.read()
content = content.replace('old', 'new')
with open('/tmp/extracted/.vite/build/index.js', 'w') as f: f.write(content)
"
# 4. Repack (Node.js API — preserves native binaries)
cat > /tmp/build.mjs << 'EOF'
import { createPackageWithOptions, getRawHeader } from '@electron/asar/lib/asar.js';
import { createHash } from 'crypto';
const src = '/tmp/extracted', dest = '/tmp/app.asar.patched';
await createPackageWithOptions(src, dest, { unpack: '{*.{node,dylib},spawn-helper}' });
const hash = createHash('SHA256').update(getRawHeader(dest).headerString).digest('hex');
console.log('Hash:', hash);
EOF
node /tmp/build.mjs
# 5. Update Info.plist (9 files!) with header hash
python3 -c "
import plistlib, os
HASH = '...'
for p in [list of 9 plists]:
with open(p,'rb') as f: pl = plistlib.load(f)
pl['ElectronAsarIntegrity'] = {'Resources/app.asar': {'algorithm': 'SHA256', 'hash': HASH}}
with open(p,'wb') as f: plistlib.dump(pl, f)
"
# 6. Deploy + Sign
cp /tmp/app.asar.patched /Applications/Claude.app/Contents/Resources/app.asar
cp -r /tmp/app.asar.patched.unpacked/* /Applications/Claude.app/Contents/Resources/app.asar.unpacked/
sudo codesign --remove-signature /Applications/Claude.app
sudo codesign -s - /Applications/Claude.app --deep --force --entitlements /tmp/entitlements.plist
Lessons Learned
--unpack-dirCLI only accepts 1 value → use glob{...}or Node.js API- 9 copies of
ElectronAsarIntegrityspread across Info.plist files - Hash is SHA256 of
headerString(JSON), not the entire file --unpackCLI is buggy for.node→ use APIcreatePackageWithOptions({unpack})- Native binaries (
.node,.dylib,spawn-helper) must stay inunpacked/ - Code signing: remove original signature, re-sign ad-hoc with entitlements
- Minimum entitlements:
com.apple.security.virtualization,com.apple.security.device.audio-input,com.apple.security.cs.allow-jit
Applied Patches (V17c final)
| # | Patch | Purpose |
|---|---|---|
| 1 | function Uy(A){return false} | 1p mode — claude.ai voice UI |
| 2 | voice:{buildGate:()=>!0} | Enable voice feature |
| 3 | if(false)return{...entitlement_missing...} | Bypass entitlements verification |
| 4 | Audio proxy handler in array C | Proxy /v1/audio/* → gateway |
| 5 | Sqi="localhost:8443" | Redirect CSP to Caddy (not effective) |
| 6 | require("../../harness-bootstrap") | Inject harness at boot |
2. Deobfuscation
Pipeline
# Deminify + unpack webpack bundle
node ~/.hermes/node/bin/webcrack index.js -o /tmp/demified/
# Result: 19MB deobfuscated.js + extracted modules
ls /tmp/demified/
# 333.js 486.js 515.js 747.js 823.js 949.js bundle.json deobfuscated.js
Key Code Discoveries
| Symbol | Meaning | Line |
|---|---|---|
Uy() | deploymentModeIs3p — returns true in 3p | 150368 |
_cr | 1p deployment mode class | 216220 |
nk | app://localhost — URL in 3p | 149817 |
Cmn() | Registers protocol handler app:// | 482078 |
k3A() | Blocked paths checker | 481540 |
qpn | List of blocked paths in 3p | 481538 |
Emn() | API stub handler (array C) | 481925 |
Sqi | a-api.anthropic.com (CSP) | 144596 |
jr.quickAccess.dictation | Swift bridge dictation | 318974 |
fKr() | Initializes Swift bridge | 319000 |
3. Bezetacil Harness
Location
~/.bezetacil/
└── scripts/
└── 001-debugger-spy.js ← auto-loaded at boot
Harness Bootstrap (injected into index.js)
require("../../harness-bootstrap");
Loads automatically when the app starts. The harness:
- Reads
~/.bezetacil/scripts/*.jsin alphabetical order - Executes each script via
eval()in the main process context - Hot reload:
fs.watch()on directory — editing file = automatic reload - Exposes global API:
__HARNESS__.eval(),__HARNESS__.log(),__HARNESS__.reload()
Harness API
// Execute code in main process
__HARNESS__.eval("require('electron').BrowserWindow.getAllWindows().length")
// Manual hot reload
__HARNESS__.reload()
// Status
__HARNESS__.status()
// → { log: '/tmp/claude-harness.log', scriptsDir: '...', pid: 89917 }
Development Cycle
1. Edit ~/.bezetacil/scripts/001-my-script.js
2. Save
3. ~3s later: automatic hot reload
4. Check /tmp/claude-harness.log for logs
Zero repacks! Only need to rebuild if changing the bootstrap itself.
Naming Convention and Hot Reload
CRITICAL RULE: Bezetacil scripts must NEVER be versioned with incremental numeric suffixes (e.g., v001-script.js, v002-script.js, 007-capture.js).
Why: The harness loads ALL *.js from the directory in alphabetical order. Each hot reload adds new handlers via eval() without removing previous ones. Versioning with incremental names causes multiple versions of the same script to coexist in memory, each registering their own event listeners — causing handler duplication, race conditions, and files generated with incorrect names.
Correct format: Use fixed descriptive names.
✅ 001-debugger-spy.js ← fixed name, rewritten when updated
✅ capture-tts-audio.js ← fixed descriptive name
✅ supports1m-autodiscovery.js ← fixed descriptive name
❌ 001-script.js, 002-script.js, 003-script.js ← NEVER do this
❌ v1-spy.js, v2-spy.js ← NEVER do this
❌ 007-capture-tts-audio.js ← NEVER do this
Guard pattern: Every script must start with a guard to prevent multiple instances after hot reload:
(function() {
// Guard: prevents multiple instances after hot reload
if (globalThis.__MY_SCRIPT_LOADED__) return;
globalThis.__MY_SCRIPT_LOADED__ = true;
// ... script ...
})();
Additional conventions:
.js— active script, loaded at boot.js.disabled— disabled script (ignored by harness).js.inject_first— injected BEFORE normal scripts (hooks that need to be in place before initialization).js.inject_last— injected AFTER normal scripts (observers, spy, loggers)
Lifecycle:
1. Edit ~/.bezetacil/scripts/my-script.js ← always the SAME name
2. Save
3. ~3s later: automatic hot reload
4. Check /tmp/claude-harness.log for logs
5. Restart app if needed to clear old handlers from memory
4. Debugger Spy
Script: 001-debugger-spy.js
Uses webContents.debugger + Fetch.enable + Network.enable to capture:
- Fetch/XHR: URL, method, headers, postData, responseBody
- WebSocket: frames sent and received (full payload)
- Network: all requests with URL and method
Output
/tmp/voice-captures.json— array of captures with timestamp/tmp/claude-harness.log— debug logs[SPY]
Capture Example
{
"ts": "2026-06-23T13:56:35.268Z",
"type": "ws",
"method": "Network.webSocketFrameReceived",
"payload": "{\"type\": \"TranscriptText\", \"data\": \"Houston, we may have a problem.\"}"
}
WebContents covered
BrowserWindow.getAllWindows()→ main webContentswebContents.getAllWebContents()→ ALL webContents (including WebContentsView)BrowserWindow.getBrowserViews()→ child views
5. CDP Tools
Python Scripts (PEP 723 — uv run)
Location: /tmp/cdp-*.py
| Script | Function |
|---|---|
cdp-eval.py | Execute JS in main process via CDP |
cdp-harness.py | Use harness API via CDP |
cdp-fetch.py | Load scripts from file server |
cdp-load-scripts.py | Orchestrate injection |
Usage Pattern
# Connect to CDP (must be active in UI)
curl -s http://localhost:9229/json | jq '.[0].webSocketDebuggerUrl'
# Execute via harness
uv run /tmp/cdp-eval.py
File Server
cd /tmp/scripts && python3 -m http.server 8765
Allows loading scripts from within Electron without string escaping.
6. Bridge Server (TCP)
Concept
Bidirectional TCP connection between Python and the Electron main process for real-time commands.
Files
/tmp/scripts/bridge-server-v2.py— async Python server (port 9876)/tmp/scripts/harness-bridge.js— TCP client in Electron
How It Works
Python (bridge-server) ←→ TCP :9876 ←→ Electron (harness-bridge)
│
├─ command: {"id":1, "code":"1+1"}
└─ response: {"id":1, "result":"2"}
7. Entitlements
File: /tmp/entitlements.plist
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.virtualization</key><true/>
<key>com.apple.security.device.audio-input</key><true/>
<key>com.apple.security.device.camera</key><true/>
<key>com.apple.security.device.bluetooth</key><true/>
<key>com.apple.security.device.usb</key><true/>
<key>com.apple.security.device.print</key><true/>
<key>com.apple.security.personal-information.location</key><true/>
<key>com.apple.security.personal-information.photos-library</key><true/>
Extracted from the original DMG at /Users/alefita/Downloads/Claude.dmg.
8. ASAR Versions
| Version | Patches | Status |
|---|---|---|
| V0 | Original (backup) | Clean base |
| V8 | Uy=false, voice gate, entitlement, audio proxy | 3p mode, no voice UI |
| V9 | V8 + Uy=false | Stable — 1p mode, voice works |
| V15 | V9 + harness v1 | Initial harness |
| V16 | V15 + Sqi localhost | Redirect attempt (not effective) |
| V17c | V16 + harness v3 (bezetacil) | Current — hot reload, debugger spy |
9. Scripts Quick Reference
# Full rebuild
cd /tmp/claude-v17 && node /tmp/deploy-v17c.mjs
# Hot reload — just edit
vim ~/.bezetacil/scripts/001-debugger-spy.js
# View logs
tail -f /tmp/claude-harness.log
# View captures
cat /tmp/voice-captures.json | python3 -m json.tool | head -50
# Re-sign (after rebuild)
sudo codesign --remove-signature /Applications/Claude.app
sudo codesign -s - /Applications/Claude.app --deep --force --entitlements /tmp/entitlements.plist