EngineeringJul 20, 2026

Reverse-engineering Claude's CLI with Kimi K3 and Ghidra

Fable would never. Kimi K3 breezed through it.

Antonio Bustamante
Antonio Bustamante
Jul 20, 2026·15 min read·Engineering·

We had heard that Kimi K3 was far less restrictive than Fable, so we decided to take advantage of it to apply it in white-hat security practices: reverse-engineering Anthropic's Claude Code CLI using Ghidra, the NSA's open-source reverse-engineering framework. This article was entirely written by Kimi K3.

To be explicit about intent: this is a fun project, not a gotcha. The CLI under analysis turned out to be an unusually well-engineered piece of software, and the honest severities of our findings reflect that. Our goal is twofold: (1) to demonstrate a reproducible methodology in which an LLM agent orchestrates a full reverse-engineering pipeline - triage, headless disassembly, payload carving, parallelized corpus analysis, and verification - and (2) to document what a modern AI CLI actually ships in its binary: its permission model, its telemetry schema, its feature flags, and its unreleased features. All findings were verified against the binary itself. All analysis was performed on software installed on the author's own machine.

Summary of findings

For readers who want the conclusions before the methodology:

Methodology. The full pipeline ran in ~1 hour on a single laptop: built-in triage (file/otool/strings) → Ghidra 11.4 headless on the 54MB native half → carving of the 181MB embedded application payload → three parallel LLM sub-agents auditing the recovered 24MB JS corpus (permission layer, auth surface, inventory) → independent verification of every headline finding against the binary. Coverage: the triage corpus captured every string ≥200 chars in the binary (6,882 lines / 24MB, of which 1,973 contain function bodies); each of the three audit passes was instructed to be exhaustive within its brief, and the orchestrator spot-checked raw regions the sub-agents flagged as dense. The native half was covered by Ghidra's full auto-analysis (Section 6).

The binary. Claude Code v2.1.214 is a Bun single-file executable: a 236MB Mach-O embedding a JavaScriptCore runtime and the entire application as recoverable source, including its own build metadata (git SHA, build time, Datadog sourcemap group).

The inventory. 1,282 telemetry events; 287 feature-flag sites forming a de facto product roadmap (including a date-gated model migration and a tool that lets the model end the conversation); hidden commands (/heapdump, /voice, /schedule, /remote-control, a /design hub); and a complete, hardened enterprise SSO gateway server embedded in the CLI.

The vulnerabilities. Two MEDIUM and four LOW findings, none catastrophic: a modulo-biased ~34.6-bit OAuth device code protected only by per-IP rate limiting (MEDIUM); an acceptEdits permission mode that auto-allows rm/mv/cp/sed without flag inspection (MEDIUM); a regex-based destructive-command warner evadable via $( )/backticks/subshells, git -C, sudo prefixes, long flags, and 10k truncation (LOW); a Unicode sanitizer that misses several invisible codepoints and throws instead of failing closed (LOW); and a read-only classification that trusts man despite its pager (LOW, hypothesis). The audited-and-clean list is longer than the findings list (Section 5.6) - this is a hardened codebase.

The modding surface. The binary exposes a large, intentional configuration surface (base URL, model remapping per tier, custom headers, Bedrock/Vertex providers, flag overrides) - Section 9 maps exactly how much of the "hardcoded" behavior is actually parametrizable without touching a single byte, and how far you can go if you do.

1. Introduction

Reverse engineering has a throughput problem. The mechanical steps - triage, unpacking, string analysis, xref chasing - are well understood, but they are slow, and the interpretive steps ("is this OAuth flow sound?", "can this regex be bypassed?") require a human who is simultaneously fluent in the domain and willing to read megabytes of minified JavaScript. Both resources are scarce.

Large language model agents change the economics. An agent with a terminal can run the mechanical pipeline end-to-end, and - more interestingly - can perform the interpretive passes at a scale no human reviewer would tolerate: auditing every regex in a permission classifier, tracing every event in a 1,282-entry telemetry taxonomy, or fanning out parallel audits over a 24MB string corpus.

We chose Anthropic's Claude Code CLI as the target for three reasons. First, it is security-relevant: it is an agent that executes shell commands on developer machines, so its permission layer is a genuinely important piece of defensive code. Second, it is technically interesting: a 236MB Bun-compiled binary embedding a JavaScriptCore runtime and the entire application as an in-binary payload. Third, we suspected it would be juicy - and it was.

2. Target characterization

bash
1$ file claude
2Mach-O 64-bit executable arm64
3$ ls -lh claude
4-rwxr-xr-x 236M claude

Load-command analysis (otool -l) reveals the structure immediately:

bash
1 | Section | Size | Contents |
2 | `__text` | ~54MB | Native arm64: Bun runtime, JSC, SQLite, TLS, tree-sitter |
3 | `__bun` | ~181MB | Embedded application bundle (JSC bytecode + source strings) |
4 | `__cstring` | ~6.6MB | C strings, incl. large contiguous JS source chunks |
5 | `__jsc_int` || JavaScriptCore internals |

The presence of __bun, __jsc_int, and the telltale string ---- Bun! ---- identifies the binary as a Bun single-file executable: the JavaScript application is not downloaded at runtime; it is in the file. This determines the entire strategy - the native half is a generic runtime, and the application logic is recoverable from embedded source without decompilation. Ghidra is pointed at the native half; carving recovers the application half.

The binary also embeds its own provenance, verbatim:

javascript
1{PACKAGE_URL:"@anthropic-ai/claude-code", VERSION:"2.1.214",
2 BUILD_TIME:"2026-07-17T23:24:50Z",
3 GIT_SHA:"e158e55a79995c80ed463a5d2de322bc0ac2f711",
4 DD_SOURCEMAP_GROUP:"darwin"}

DD_SOURCEMAP_GROUP is a small gift: it implies per-build sourcemaps exist in Anthropic's Datadog. We did not pursue this (out of scope and out of bounds), but it is the kind of artifact a less scrupulous analyst would note.

3. Methodology

The pipeline, in execution order. Wall-clock time approximately one hour, the majority unattended.

3.1 Triage (built-in tools, ~2 min). file, otool -l, strings | grep -i bun. Output: strategy selection (native vs. embedded payload).

3.2 Headless disassembly (Ghidra, background). Ghidra 11.4 (OpenJDK 21) against the full binary:

bash
1export JAVA_HOME=/opt/homebrew/opt/openjdk@21
2~/tools/ghidra_11.4_PUBLIC/support/analyzeHeadless ~/re/ghidra_proj ClaudeCLI \
3 -import /path/to/claude -max-cpu 6

The orchestration principle: never watch Ghidra work. analyzeHeadless runs in the background; the agent does useful work in the foreground; post-analysis is scripted via -postScript (Section 6).

3.3 Payload carving. Section file offsets come straight from the Mach-O load commands; carving is a seek-and-copy (Python, since macOS dd lacks skip_bytes):

python
1off, size = 64831488, 0x000000000ad260b9 # __bun: offset, size
2with open(B,"rb") as f, open(out,"wb") as o:
3 f.seek(off); o.write(f.read(size)) # 181,559,481 bytes

The __bun section is binary (JSC bytecode, transpiler cache), but the application source is embedded alongside it as strings. Extracting the analyzable corpus:

bash
1strings -a -n 200 claude > long_strings.txt # 6,882 lines, 24MB
2grep -c 'function' long_strings.txt # 1,973 chunks contain function bodies

3.4 Parallel corpus analysis. 24MB of minified JS exceeds human patience but not an LLM's. Three sub-agents were fanned out with disjoint hunting briefs:

1. Permission/safety layer: the destructive-command classifier, the bash read-only classifier, the Unicode sanitizer. 2. Auth surface: the embedded OAuth/OIDC gateway, end to end. 3. Inventory: feature flags, telemetry taxonomy, hidden tools, endpoints, credential handling.

3.5 Independent verification. Every headline claim from a sub-agent was re-verified by the orchestrator against the binary before inclusion. Sub-agent summaries are self-reports; the binary is ground truth. Two examples of verification greps appear in Sections 5.1 and 5.2.

4. The inventory: what a CLI ships when it thinks nobody is looking

This section is descriptive, not accusatory - but it is the part of the analysis that would have been impossible without automation.

4.1 Telemetry. 1,282 unique event names, uniformly prefixed tengu_ (a tengu is a Japanese trickster spirit; the project's internal codename is apparently not a secret). The taxonomy is comprehensive: permission decisions (tengu_tool_use_granted_by_classifier), MCP lifecycle, background daemon, compaction, plugins, billing, and 22 "dead probe" canaries. Error reporting goes to Datadog RUM; machine identity is derived from IOPlatformUUID; a snapshot of up to ~50 boolean feature-flag values rides along with events. Sensitive fields are visibly passed through enum/hash helpers before emission - someone thought about privacy here, deliberately.

4.2 Feature flags. 287 flag read sites, GrowthBook-style (et("tengu_...", default)). Flags are, in effect, a roadmap. A selection:

bash
1 | Flag | What it gates |
2 | `tengu_kairos_push_notifications` | an unseen PushNotification tool |
3 | `tengu_umber_kestrel` | an `EndConversation` tool — the model may hang up on you |
4 | `tengu_sunset_penguin_opus47 = "2026-07-25"` | a date-gated model migration |
5 | `tengu_ultraplan_timeout_seconds = 5400` | "ultraplan" may think for 90 minutes |
6 | `tengu_slate_ibis` | Explore/Plan sub-agents |

4.3 Hidden surface area. /heapdump (isHidden: true; writes a JSC heap snapshot to the user's Desktop - a gift for future analysis), /voice, /schedule (cloud cron agents), /remote-control (session exposure to mobile), a /design hub with its own OAuth token flow, ObserverReport/PushNotification/SendFeedback tools, and a bundled beta header managed-agents-2026-04-01.

4.4 A server in a trench coat. The largest single discovery: the CLI embeds a complete multi-user enterprise SSO gateway - OIDC device-code authorization, a Postgres-backed audit and spend store, managed-policy serving, per-IP rate limiting, CSRF checks, HSTS, CSP with nonces, and an SSRF guard that explicitly blocks link-local addresses, metadata.google.internal, and fd00:ec2::254. This is not a stub; it is a hardened auth proxy that happens to live inside a terminal application.

5. Security analysis

Severity ratings are ours, assigned conservatively and honestly. The overall engineering posture of this codebase is strong; the findings below are the exceptions, and most are exceptions precisely because the surrounding code is careful.

5.1 [MEDIUM] Gateway device codes: ~34.6 bits of entropy, modulo-biased

The gateway's human-typed device code is generated as follows - verbatim, as recovered from the binary and independently re-verified by grep:

javascript
1function fZf(){let e=Lti.randomBytes(8),t="";for(let r=0;r<8;r++){
2 if(r===4)t+="-";t+="BCDFGHJKMNPQRSTVWXYZ"[e[r]%20]}return t}

Two weaknesses compound. (a) The space is 20^8 ≈ 2^34.6 codes. (b) e[r] % 20 on a uniform byte exhibits classic modulo bias: 256 mod 20 = 16, so 16 of the 20 symbols carry probability 13/256 versus 12/256 - an ~8% relative skew that slightly reduces effective entropy.

Mitigations exist: codes expire in 600s, and verification is rate-limited (10 attempts / 10 min). However, the verify limiter is keyed only on client IP, and user codes are stored as plaintext lookup keys. Against a distributed guesser, the effective budget is 10 guesses per source IP per window against the aggregate pending-code space. For an enterprise SSO gateway, 34 bits is thin; 10–12 characters, or rejection sampling over a larger alphabet, would close this.

In fairness, the surrounding flow is exemplary and deserves to be quoted: the state parameter is an algorithm-pinned JWE (dir/A256GCM, 300s expiry); the browser binding is length-checked and compared with crypto.timingSafeEqual; session JWTs pin algorithms:["HS256"] with fixed audience (no algorithm-confusion, no none); PKCE is supported (though optional - LOW); and the userinfo fallback enforces sub equality with the id_token. Finding 5.1 is notable because everything around it is tight.

5.2 [MEDIUM] acceptEdits mode auto-allows rm with no flag inspection

The permission mode acceptEdits is documented as auto-approving file edits. The auto-allow list recovered from the binary (verified verbatim):

javascript
1["mkdir","touch","rm","rmdir","mv","cp","sed"]

No flag inspection applies to these. rm -rf build/ passes in acceptEdits mode; only a separate "dangerous target" heuristic (root/home/wildcard paths) remains. A mode named "accept edits" silently extending to arbitrary deletion within the working directory is a principle-of-least-surprise violation, and the name is doing security-relevant work: users grant it believing it is narrower than it is.

5.3 [LOW] The destructive-command classifier is regex-shaped — and evadable

The CLI warns users when a command looks destructive, via a regex table feeding advisory warnings, target-scope analysis, and telemetry. The warnings are what a skimming human trusts. Verified evasions:

  • Boundary gap. Command boundaries are [;&|\n] — but not $(, backtick, or (.

echo $(rm -rf x), ` rm -rf x , and ( rm -rf x ) match nothing. (The PowerShell table includes ({`; the bash table does not. Inconsistent.)

  • Adjacency assumption. git -C repo reset --hard, sudo rm -rf, env rm -rf,

xargs rm all evade patterns requiring git\s+reset / separator-then-rm adjacency.

  • Long flags. rm --recursive --force matches nothing; only short-flag shapes exist.

  • Truncation. Input is sliced at 10,000 chars before matching, so

# <9.9k chars of comment>; rm -rf x pushes the payload past the slice.

  • cd-tracking. The cwd tracker recognizes cd X &&/; only; cd /tmp | rm -rf .

desynchronizes it.

Severity is LOW because the classifier is advisory rather than a gate - but it is the advisory layer a fatigued reviewer leans on, and the same regexes feed scope analysis and telemetry signals.

5.4 [LOW] Unicode sanitizer: invisible characters survive; failure mode is a throw

The CLI ships an iterated NFKC + format-character stripper for adversarial text - genuinely good hygiene. It strips Cf/Co/Cn and the classic bidi/zero-width ranges, but not U+034F (combining grapheme joiner), U+2800 (braille blank), Hangul fillers (U+115F/U+1160/U+3164), or variation selectors. These render invisibly and survive - a steganographic instruction channel in content an MCP server hands to the model. Additionally, exceeding 10 normalization iterations throws rather than failing closed: a crash primitive on adversarial input wherever a caller lacks a try/catch.

5.5 [LOW, hypothesis] Read-only classification trusts man

man and help are classified read-only with argument-path checks, but man spawns a pager, and less offers !sh. No PAGER neutralization was visible in the binary. Auto-approved man ls → interactive shell, in principle. Flagged as a hypothesis pending dynamic confirmation.

5.6 What was audited and found clean

Honesty requires listing the things we tried to break and could not: JWT algorithm pinning (no confusion possible), encrypted OAuth state, timing-safe browser binding, sub-mismatch enforcement on userinfo fallback, single-use device grants, CSRF via sec-fetch-site/origin checks, CSP with nonces, the SSRF guard, credential storage (macOS Keychain via the security CLI, with config-file fallback), and refresh-token handling (delegated rotation; revoked tokens are actively cleared). This is a hardened codebase.

6. Native-layer analysis with Ghidra

Full auto-analysis of the 236MB Mach-O completed headless in 47 minutes (2,799s; breakdown: 1,298s on non-returning-function discovery alone, 555s on decompiler switch analysis, 55s on raw disassembly of the 54MB __text). Yield: 72,108 functions. Three results matter.

6.1 The bundle has exactly one front door. Tracing references to the __bun block (0x103f98000..0x10ecbe0b8, 181,559,481 bytes) from 72k functions yields a single native accessor - a 24-byte thunk:

bash
1undefined * FUN_10031a418(void)
2{
3 return &DAT_103f98000; // base address of the embedded bundle
4}

called from precisely two startup functions (FUN_1014e6960, FUN_100ea579c). The entire 181MB payload - every prompt, flag, and permission rule - enters the runtime through this one address. For a researcher this is the ideal choke point: hook or patch one thunk and you control which bytes the JavaScriptCore engine believes the application is. It is also where a cache-integrity check would live if one existed (see §9.3's note on the transpiler cache); we found no evidence of one in the accessor path.

6.2 The two layers are hermetically separated. Scanning all 36,389 defined strings in __cstring for application-layer markers (---- Bun! ----, tengu_, find-generic-password, metadata.google.internal, BUN_CONFIG_TOKEN) returns zero native cross-references: no native function ever takes the address of an application string. The native runtime treats the payload as opaque data consumed wholesale by JSC - confirming the two-layer architecture inferred in Section 2 and validating the carve-first strategy: native decompilation would never have surfaced the application logic.

6.3 What the native half actually is. The 54MB of __text is Bun's runtime: JavaScriptCore, the SQLite layer used for session transcripts, TLS, tree-sitter, and the transpiler. Demangled Rust and C++ symbols abound. The security-relevant native surface is therefore the generic Bun/JSC layer - meaning any memory-safety question here is upstream Bun's, not Anthropic's, and the application-specific trust boundaries (§5) all live in the carved JavaScript.

Post-analysis was fully scripted - the Ghidra equivalent of the agent's grep loop:

bash
1analyzeHeadless ~/re/ghidra_proj ClaudeCLI -process 2.1.214 \
2 -noanalysis -postScript HuntXrefs.py # function count, __bun refs, string xrefs
3analyzeHeadless ... -postScript DecompLoader.py # decompile the bundle thunk

7. Ethics, scope, and disclosure posture

All analysis was static, local, and performed on a binary installed on the author's own machine. No network services were probed; no third-party infrastructure (including the Datadog sourcemap hint) was touched; no hidden features were enabled. The findings are the kind any competent audit would surface, and none is a smoking gun - several are arguably design trade-offs the vendor made consciously. We describe them because they are instructive, not because they are damning. If anything, this exercise raised our opinion of the target.

9. So you read the findings — now what? The practitioner's playground

The most common question after any RE writeup is "okay, but what can I do with this?" For this target, quite a lot. We divide the playground into three tiers by how invasively you must touch the binary.

9.1 Tier 0: the intended configuration surface (patch nothing)

The single most surprising result of the inventory pass is how little is actually hardcoded. The binary reads a large, deliberate environment surface - this is a supported configuration API, hiding in plain sight:

bash
1# Point the whole CLI at your own endpoint / proxy / gateway
2ANTHROPIC_BASE_URL=https://my-proxy.internal
3
4# Remap the model tiers to anything your endpoint serves
5ANTHROPIC_MODEL=claude-...
6ANTHROPIC_DEFAULT_OPUS_MODEL=... # 35 occurrences in the binary
7ANTHROPIC_DEFAULT_SONNET_MODEL=...
8ANTHROPIC_DEFAULT_HAIKU_MODEL=...
9ANTHROPIC_DEFAULT_FABLE_MODEL=... # yes, there's a Fable tier
10ANTHROPIC_SMALL_FAST_MODEL=...
11
12# Inject arbitrary headers (auth, tracing, tenant routing)
13ANTHROPIC_CUSTOM_HEADERS="X-Tenant: blue"
14
15# Third-party providers are first-class: Bedrock and Vertex toggles exist
16CLAUDE_CODE_USE_BEDROCK=1 ANTHROPIC_BEDROCK_BASE_URL=...
17CLAUDE_CODE_USE_VERTEX=1
18
19# Kill the feature-flag/beacon fetch entirely
20DISABLE_GROWTHBOOK=1
21CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

Practical experiments at this tier: (a) stand up a local TLS-terminating proxy, set ANTHROPIC_BASE_URL, and observe the complete client request schema - the system prompt, tool definitions, and context assembly all cross the wire you now own; (b) remap tiers per task, e.g. force the cheap tier everywhere with ANTHROPIC_SMALL_FAST_MODEL; (c) route through the embedded gateway (Section 4.4) - it exists precisely so enterprises can interpose here. Changing providers, therefore, requires zero reverse engineering: the vendor built the door and left it unlocked.

9.2 Tier 1: flags and settings (patch nothing, but use the map)

Section 4.2's flag table is a control panel. GrowthBook-style flags typically resolve from a local cache/settings file before the network, and et(name, default) means the default applies when no source is reachable. Fun, legal experiments: enumerate which flags are locally overridable; flip tengu_kairos_push_notifications and friends in a disposable profile and diff the exposed tool list; set CLAUDE_CODE_SIMPLE=1 (16 occurrences) to force the no-OAuth/no-keychain path and study the credential fallback chain. The hidden /heapdump command deserves special mention: it writes a JSC heap snapshot to your Desktop - feed it to a heap viewer and you have the live object graph of the application, complementing our static corpus.

9.3 Tier 2: binary patching (your machine, your rules — with caveats)

The embedded application is strings inside a signed Mach-O, which invites the obvious question: can you edit the prompts, the classifiers, the flags in place? Technically:

  • String patching. Same-length edits to the embedded JS are trivially applied to

the carved region and written back at the computed file offset (Section 3.3 gives you the exact arithmetic). Shorter strings padded with spaces/comments often survive minified parsers fine.

  • Code-signing. Any modification invalidates the signature; on macOS you re-sign

ad-hoc (codesign --sign - --force claude). Gatekeeper entitlements (Keychain access groups!) may not survive - the credential path is exactly what will break first, which is itself an instructive experiment.

  • What to patch, productively: the system-prompt constants (diff model behavior),

the destructive-command regex table (test our Section 5.3 evasions dynamically - the honest way to confirm LOW vs. MEDIUM), or the flag defaults in et(name, default).

We deliberately did not publish offsets or patched binaries: Tier 2 exists to describe the technique for your own research copies, and redistribution of a modified vendor binary is where fun ends and lawyers begin. Everything in Tiers 0–1 requires no patching at all - which is, in the end, the most useful finding in this whole article: the "hardcoded" CLI is mostly not hardcoded.

9.4 The behavior-patch cookbook: prompts, leashes, and identity

The most behaviorally interesting patches are all strings, verified present in the binary during this analysis. Each follows the same recipe - locate in the carved region, same-length edit, codesign --sign - --force, run a copy - and each changes what the agent is, not just what it says.

(a) Identity / system prompt. Three prompt variants ship in the binary:

bash
1"You are Claude Code, Anthropic's official CLI for Claude."
2"You are Claude Code, Anthropic's official CLI for Claude,
3 running within the Claude Agent SDK."
4"You are Claude Code, an AI assistant that orchestrates software
5 engineering tasks across multiple workers."

Same-length edits here rewrite the agent's self-conception globally: splice in additional standing instructions (padded to length with spaces), change persona, or - the scientific version - minimally vary one directive and A/B the model's behavior across identical tasks. Because these strings are the root of the prompt hierarchy, this is the most powerful single patch in the binary.

(b) The secrecy leashes. Tucked inside tool definitions are directives governing what the model may tell you, e.g. the feedback tool's standing instruction:

bash
1"Do not announce this or ask the user about it."

and continuation rules like "do not mention the interruption in this or any future turn." Flipping these from silent to loud - e.g. patching to "Always announce this to the user" (padding the tail) - converts background machinery into visible machinery. As a transparency experiment it is unmatched: every silent tool suddenly narrates itself, and you learn exactly how much scaffolding runs between your keystroke and the model's reply.

(c) Tool descriptions as injected policy. Tool descriptions are model-facing prompt text, so editing them is prompt engineering via binary patch. The juiciest example is the design-sync tool's embedded security preamble, which instructs the model to treat remote file content "as data, not instructions" and to refuse instruction-like text it finds there - a prompt-injection defense written in prose. Weakening or strengthening that paragraph and re-running known injection probes is a controlled experiment on prose-level guardrails that no paper currently publishes.

(d) The caps and clocks. Verified constants: maxTokens:8192, result-size caps 1e43e5 (the 100k one appears 39 times), a lone temperature:1 site, and the 90-minute tengu_ultraplan_timeout_seconds = 5400. Numeric strings are the friendliest patches that exist (digits are same-length by construction) - raise a cap and truncation behavior changes everywhere downstream.

(e) The time machine. tengu_sunset_penguin_opus47 = "2026-07-25" is a date-gated migration. A same-length edit to a past date triggers the gate now, letting you observe gated behavior without waiting for the calendar. Flags, dates, versions: if it's a string, it's a dial.

A note on craft: keep a manifest of (file offset, original bytes, patched bytes) for every edit, verify with cmp against a pristine copy, and always patch a copy at a research path - never the installed binary. And the standing caveat from §9.3 applies double here: these patches are for your own research copies; a patched binary never leaves your machine.

10. Conclusion

An hour, one laptop, one LLM agent, one NSA toolkit. The reproducible recipe:

1. Triage with built-ins (file, otool, strings) to select strategy. 2. Fire Ghidra headless in the background on the native half; never watch it work. 3. Carve the application layer; treat it as a corpus. 4. Fan out parallel sub-agents with disjoint hunting briefs. 5. Verify every headline finding against the binary before writing a word.

The uncomfortable general takeaway for anyone shipping AI tooling: your compiled CLI is your source code, your roadmap, your telemetry schema, and your unreleased features, sitting in ~/.local/bin. And the agent reading it might not be the one you shipped.

Appendix A: Reproduction commands

bash
1B=~/.local/share/claude/versions/2.1.214
2file $B; otool -l $B | grep -A3 sectname
3strings -a -n 200 $B > long_strings.txt
4grep -o 'function fZf(){[^}]*}' long_strings.txt
5grep -o '\["mkdir","touch","rm","rmdir","mv","cp","sed"\]' long_strings.txt
6export JAVA_HOME=/path/to/openjdk@21
7/path/to/ghidra_11.4_PUBLIC/support/analyzeHeadless ~/ghidra_proj ClaudeCLI \
8 -import $B -max-cpu 6

Artifacts: long_strings.txt (24MB analysis corpus), a structured inventory file, and the Ghidra project (72,108 analyzed functions).

Methodology: target claude CLI v2.1.214 (darwin/arm64); Ghidra 11.4 headless on OpenJDK 21; corpus analysis by three parallel Kimi K3 sub-agents; all headline findings independently verified against the binary by the orchestrating agent. Written by Kimi K3.

Antonio Bustamante

Written by

Antonio Bustamante

Jul 20, 2026 · Engineering

CTA accent 1CTA accent 2

Ready to see it in action?

Talk to our team to walk through how Bem can work inside your stack.

Talk to the team
Reverse-engineering Claude's CLI with Kimi K3 and Ghidra | bem