AI Coding Agent Harnesses: A Runtime Field Report
Seven coding agents are installed on this machine. They share one job, talking to a language model, but they reach it through five different runtime strategies. This report is the consolidated forensic record of what each one is, how we proved it, and why the runtime choice matters less than you would think.
Executive summary #
The headline finding: "Does it run on Node?" has no single answer here. Two harnesses run on an external Node interpreter, one runs on Python, two are JavaScript compiled into a self-contained native binary, one is pure Rust, and one is a desktop app that runs Node and Rust at the same time. You own a complete cross-section of how 2026-era agent tools ship.
| Harness | Form | Source | Runtime reality | On PATH |
|---|---|---|---|---|
| Gemini CLI | CLI | TS/JS | NODE external interpreter | gemini |
| pi | CLI | TS/JS | NODE external interpreter | pi |
| hermes-agent | CLI | Python | CPYTHON venv | hermes |
| Claude Code | CLI | TS/JS | COMPILED-JS Bun / JavaScriptCore | claude |
| OpenCode | CLI | TS/JS | COMPILED-JS Bun | opencode |
| Grok CLI | CLI | Rust | RUST native machine code | grok |
| Codex | Desktop app | TS/JS + Rust | HYBRID Electron shell + Rust core | (app bundle) |
Node-based and visible on PATH: Gemini CLI and pi. JavaScript at the source but compiled to a dependency-free binary: Claude Code and OpenCode. True systems-language native: Grok (Rust). Python: hermes. The only GUI app, and the only one running two runtimes at once: Codex.
Five runtime strategies #
Every agent in this audit is some flavor of "JavaScript or Python or Rust talking to an API." What separates them is how the runtime is delivered to your disk. There are five distinct answers, and knowing which is which tells you the tool's startup speed, its install footprint, and what can break it.
node shebang. Needs a separate Node install on your PATH. Easiest to ship, most fragile to version drift. Gemini CLI, pi.--compile, which embeds JavaScriptCore. Claude Code, OpenCode.The market is drifting from strategy 1 toward strategies 3 and 4. Compiled binaries launch instantly and install as one file, which removes the entire class of "wrong Node version" and "broken venv" support tickets. The tradeoff is a larger download and a build pipeline the vendor has to maintain.
How we fingerprinted each runtime #
None of these tools advertises its runtime. We read it off the binary itself. The method is three cheap checks, in order, and each one resolves a different class of program.
Step 1: the first line
A text launcher starts with a shebang that names its interpreter. This alone settles the interpreted cases.
head -1 "$(command -v <tool>)"
#!/usr/bin/env node -> Node script
#!/usr/bin/env bash -> wrapper, follow what it exec's
#!.../python3.14 -> Python script
Step 2: the file type
If there is no readable shebang, the file is compiled. file confirms it is a Mach-O executable, but Mach-O does not tell you the language. That needs step 3.
Step 3: embedded string markers
Compilers leave their fingerprints inside the binary as plain strings: standard-library paths, dependency registries, engine internals. Grepping for them is a reliable language test.
| Marker found in binary | Verdict |
|---|---|
| __jsc_int, __jsc_opcodes, __wtf_config | Compiled JavaScript (JavaScriptCore via Bun) |
| /rustc/<hash>/library/..., .cargo/registry, *.rs | Rust |
| go1.x, "Go build ID" | Go |
| __eh_frame + __gcc_except_tab + .rs paths | Rust (unwind tables plus source paths) |
| electron-persisted-atom-state, "Electron Framework" | Electron (Node + Chromium) |
A binary containing the literal string node_modules is not proof it runs on Node. Grok's Rust binary ships gitignore-style patterns like "**/node_modules/**" as search data. And the Unix partition tool /usr/sbin/gpt is not an AI agent. Match on engine internals, not on incidental strings.
Gemini CLI #
Google's terminal agent. The cleanest example of strategy 1: a script whose shebang points straight at the Homebrew Node binary.
| Location | /opt/homebrew/bin/gemini |
|---|---|
| Runtime | NODE external interpreter |
| Evidence | shebang: #!/opt/homebrew/opt/node/bin/node |
Because the shebang hard-codes the Homebrew Node path, this tool is directly exposed to your system Node. A brew upgrade node changes the interpreter under it.
pi #
An open coding agent by @mariozechner, installed as a global npm package. The binary on PATH is named pi, not pi-coding-agent, which is why a search by package name can miss it. npm registers the CLI under the bin field, which often differs from the package name.
| Package | @mariozechner/pi-coding-agent@0.70.5 |
|---|---|
| Location | ~/.npm-global/bin/pi |
| Runtime | NODE external interpreter |
| Evidence | shebang #!/usr/bin/env node · engines: node >=20.6.0 |
| Note | Default provider is google (Gemini), configurable per call |
The ~/.npm-global prefix is a user-level global install location, which is why pi resolves without sudo. The declared floor of Node 20.6 is the release that stabilized built-in .env loading.
hermes-agent #
The only Python agent in the set. The command on PATH is a tiny bash launcher whose only job is to isolate the environment and hand off to a Python virtualenv.
#!/usr/bin/env bash
unset PYTHONPATH # scrub inherited config so the venv stays clean
unset PYTHONHOME
exec ".../.hermes/hermes-agent/venv/bin/hermes" "$@"
| Launcher | ~/.local/bin/hermes (bash) |
|---|---|
| Real entry | ~/.hermes/hermes-agent/venv/bin/hermes |
| Runtime | CPYTHON 3.14 in a venv |
The unset PYTHONPATH and unset PYTHONHOME lines are the giveaway it is Python: a venv only stays isolated if no inherited variable redirects the interpreter to system packages. Using exec replaces the shell process rather than spawning a child, so Ctrl-C reaches the Python agent directly.
Claude Code #
JavaScript at the source, but it does not run on your Node. The binary is a self-contained native executable with a JavaScript engine baked in.
| Location | ~/.local/bin/claude |
|---|---|
| File type | Mach-O 64-bit executable arm64 |
| Runtime | COMPILED-JS embedded JavaScriptCore (Bun) |
| Evidence | sections __jsc_int, __jsc_opcodes, __wtf_config |
Those __jsc_* and __wtf_config sections are JavaScriptCore internals (WTF is WebKit's "Web Template Framework"), the signature of a Bun-compiled standalone binary. The practical upshot: Claude Code carries its own engine and does not depend on a separate node being present. Historically it shipped as an npm package that did run on system Node; the native binary is the newer, dependency-free distribution.
OpenCode #
Same strategy as Claude Code: TypeScript source compiled to a native binary, no external interpreter.
| Location | ~/.opencode/bin/opencode |
|---|---|
| File type | Mach-O native executable (Bun-compiled) |
| Runtime | COMPILED-JS |
OpenCode and Claude Code both report as native Mach-O binaries, yet are JavaScript inside. This is the case that breaks the naive "compiled means systems language" assumption.
Grok CLI #
xAI's terminal agent, and the first genuinely non-JavaScript harness in the set. Same Mach-O file type as Claude Code, completely different innards: Rust machine code with no JS engine anywhere.
| Location | ~/.grok/bin/grok (symlink into ~/.grok/bundled/) |
|---|---|
| File type | Mach-O 64-bit executable arm64 |
| Runtime | RUST native machine code |
| Evidence | /rustc/6b00bc38... · .cargo/registry · crates: serde, serde_json, aes, addr2line |
The Rust standard-library paths and crate names are embedded for panic and backtrace support. One detail worth noting: the crates were pulled from git.twitter.biz, xAI's internal Cargo registry, not the public crates.io. The binary was compiled inside xAI's own build infrastructure. The ~/.grok/bin/grok entry shows a 31-byte size in ls because it is a symlink; the multi-megabyte real binary lives under ~/.grok/bundled/.
Codex #
The most interesting install. Codex is not a terminal CLI at all. It is an Electron desktop app, and it runs two runtimes at once.
| App | /Applications/Codex.app |
|---|---|
| Config/state | ~/.codex (auth, logs, config.toml, plugins) |
| UI shell | NODE Electron (Node + Chromium + V8) |
| Agent engine | RUST /Applications/Codex.app/Contents/Resources/codex |
| Evidence | electron-* state keys · "Codex Framework.framework" (renamed Electron) · Sparkle.framework · engine: index.crates.io, addr2line, aes, aho-corasick |
The .codex-global-state.json file is full of Electron fingerprints (electron-persisted-atom-state, electron-main-window-bounds) along with references to gpt-5.5, guardian and full-access permission modes, and bundled plugins for documents, spreadsheets, presentations, and browser-use. The bundled engine binary is Rust, but compiled on a public CI runner (/Users/runner/) from public crates.io, a different supply chain than Grok's internal registry.
Every other tool answered to command -v. Codex did not, because a .app bundle keeps its binary inside Contents/Resources/ and does not drop a shim on PATH unless you install the CLI helper. The tell that it was present anyway: a live, freshly written ~/.codex state directory. State directory without a PATH binary means a GUI app, not a CLI.
So the Codex desktop shell genuinely runs on Node, by way of Electron. The part doing the coding work is the Rust child process it launches. The Node layer is the window; the Rust layer is the engine.
Node vs Python vs Rust: which is more efficient? #
The honest answer for this class of tool: it barely matters, because agent harnesses are network-bound, not CPU-bound. More than 95 percent of wall-clock time is spent waiting on the model's HTTP response. Whatever the runtime, it is idle on a socket. The model is the bottleneck, not the interpreter.
Where real differences do exist:
| Dimension | Node (V8) | Python (CPython) | Rust / native |
|---|---|---|---|
| Raw compute | JIT, fast | Interpreted, slower | Fastest |
| Cold start | Tens of ms | Slower with heavy imports | Near-instant |
| Concurrency | Event loop, native fit | asyncio, GIL-bound for CPU | Threads, no GIL |
| Idle memory | Heavier baseline | Lighter | Lightest |
| Install footprint | Needs Node present | Needs Python + venv | Single file |
For agent CLIs, "efficiency" is mostly about startup and distribution, not steady-state throughput. That is exactly why the leading tools are converging on compiled native binaries: instant launch, single-file install, no "is the runtime the right version" support burden. The real performance lever is not Node vs Python at all. It is how the harness manages tokens and round-trips: prompt caching, parallel tool calls, context compaction. A well-designed Python agent beats a careless Node one every time, because both are racing the same network.
Bottom line. If you are optimizing for snappy startup and zero dependency hassle, the compiled-native harnesses (Claude Code, OpenCode, Grok, and Codex's Rust core) have the edge over the raw-Node and raw-Python ones. For the actual work of talking to a model, Node vs Python is a rounding error next to which model you point it at and how smartly it batches its calls.
Reproduce it yourself #
The full fingerprinting flow, runnable on any of these tools. Replace <tool> with the command name.
# 1. Where is it, and is it a script or a binary?
command -v <tool>
file "$(command -v <tool>)"
head -1 "$(command -v <tool>)" # readable shebang means interpreted
# 2. If it's a wrapper, see what it really runs
cat "$(command -v <tool>)"
# 3. If it's a compiled binary, read the language markers
B="$(command -v <tool>)"
strings -n 6 "$B" | grep -iE "rustc|\.cargo|go build id|javascriptcore|wtf_config" | head
# 4. GUI app with no PATH entry? Look for a live state dir
ls -la ~/.<tool> 2>/dev/null
mdfind "kMDItemFSName == '<tool>' && kMDItemContentType == 'public.unix-executable'"
System facts at audit time
- Host Node:
v26.0.0at/opt/homebrew/bin/node(Homebrew). - Platform: macOS (Darwin 25.5.0), Apple Silicon (arm64).
- pi version:
0.70.5. hermes Python:3.14. Codex model seen in state:gpt-5.5.
A single brew upgrade node affects every external-Node tool at once (Gemini CLI, pi, and Codex's Electron shell pin their own Node so it does not), while the compiled-JS and Rust binaries (Claude Code, OpenCode, Grok) are immune. Worth remembering if a Node update ever breaks one tool but not another.