What ChatGPT Work Can Really Do: Reverse Engineering a Binary From My Phone, On a Walk

I left the house for a walk. I dictated one paragraph into my phone. Seventy-five minutes later I had a fully analyzed Ghidra project, fifteen recovered function names and prototypes, a patch for a real bug in my own code, and a compiling C reconstruction of a stripped binary — in a zip file, waiting for me.


The part nobody tells you: it’s a VM

ChatGPT Work is the successor to Agent mode — the thing you used to trigger by typing @Agent. The rename made it sound like a productivity feature. It isn’t. When you’re in ChatGPT Work in the mobile app, you have a Linux VM.

That is the whole story. Not a sandbox that draws a chart and forgets it — a real machine with a shell, a persistent filesystem, network access, compilers, and 50+ GB of disk. Once you internalize that, your prompts change shape. You stop asking for explanations and start asking for work:

Clone this repo. Read its install docs. Build it. It’ll break — figure out why and fix it. Then actually run it against something real.

And because it’s a VM with a filesystem, there’s a natural ending to every session: ask for a zip. Everything the machine produced comes back to you as a download, ready to open on your desktop. Clone → build → experiment → zip. That loop fits in a walk.

I documented what’s inside that machine separately — CPU, RAM, disk, toolchains, and everything the sandbox refuses to show you — in Full system inventory of the ChatGPT Work VM. This article is what happens when you use it.

The ask

I wanted to see it do something genuinely hard. Not "write me a script" hard — reverse engineering hard.

I have a project called GhidraSQL: an agentic connector that exposes Ghidra, the NSA’s software reverse engineering suite, as a SQL interface. The binary becomes a database you can SELECT, UPDATE, and JOIN — functions, call graph, strings, and decompiled pseudocode all queryable, findings written straight back into the project. It’s one of the xSQL family of tools I presented at REcon 2026, and it’s built for exactly this kind of agent-driven analysis — but it needs Ghidra, a JDK, Gradle, and CMake to even start.

So I dictated this into my phone (note the typos in the transcription) and kept walking:

Go to GitHub under user name 0xEB, Elias Bachaalany. There’s a project called GhidraSQL, which relies on another project called Ghidra. Anyway, clone GhidraSQL, and it has an install prompt, markdown file, follow the instruction, and sets it up and builds it. Catch all the errors if there are errors with respect to the installation steps. Figure it out, and try to run GhidraSQL executable in headless mode. Pick a binary of your choice, and see if you can query the binary. And start with a terminal shell. That’s it, to start with a terminal shell and then issue the git clone command. You don’t need any GitHub skills or any GitHub connector.

The dictated prompt and the first minutes of work

The prompt as dictated on a walk. "GidraSQL" in the screenshot is a voice-transcription artifact, corrected above.

Note what that prompt does and doesn’t do. It doesn’t specify a binary — I said "pick a binary of your choice." It doesn’t tell it how to fix anything. It explicitly forbids the shortcut ("you don’t need any GitHub skills or any GitHub connector") because I wanted to see plain git in a plain shell. And it pre-authorizes the hard part: catch all the errors, figure it out.

Then I put the phone in my pocket.

52 minutes, unattended

It opened a terminal, cloned at commit 33d5dea, and read the repo’s install-prompt.md end to end before touching the environment. Then preflight:

Preflight caught three real blockers before the build: the java launcher is present but cannot locate libjli.so, while CMake and Gradle are absent. GCC 13, Git, Python 3.12, and disk space are fine.

Every one of those is confirmed in the environment inventory: Java ships as a launcher with a missing runtime library, and CMake and Gradle simply aren’t installed. A less careful agent would have run cmake and face-planted. This one checked first, then went shopping:

I’m repairing preflight with portable, checksum-pinned toolchains inside the workspace: Temurin JDK 21.0.12, CMake 3.31.8, Gradle 8.14.3, and the official Ghidra 12.1.2 distribution. This avoids modifying the base system and matches the runbook’s version gates. Downloads total about 930 MB.

Checksum-pinned. Workspace-local. Version-gated against my own runbook. It searched 8 websites to confirm current release versions first. That is what a competent engineer does on an unfamiliar box, and it did it while I was looking at trees.

The walls — and how it got over them

This is my favorite part of the whole run, and it’s the part that proves nobody scripted this in advance. Eight distinct failures, each diagnosed and fixed. Straight from report.md:

Stage Observed failure Cause Resolution
Toolchain discovery Required JDK 21, modern CMake, and Gradle were absent or unusable from the system path Host tools did not satisfy the install prompt Installed workspace-local OpenJDK 21.0.12, CMake 3.31.8, Gradle 8.14.3
Java loader libjli.so: cannot open shared object file The relocatable JDK’s private libraries were not on the loader path Supplied the JDK lib and lib/server directories through LD_LIBRARY_PATH
Temporary files Java/native steps could not rely on a normal /tmp The runtime had no usable conventional temporary directory Used workspace .tmp through TMPDIR and -Djava.io.tmpdir
Dependency transport HTTPS dependency resolution/downloads failed or were interrupted Restricted/proxied transport; Java trust did not initially match Used Git transport, resumable downloads, and a workspace-local Java trust setup
Parallel native build The assembler received truncated input during a concurrent build Resource pressure interrupted a compiler process, leaving an incomplete assembly stream Removed only the failed target output and rebuilt serially
Ghidra launch support/launch.sh failed where Bash process substitution required /dev/fd /dev/fd was unavailable in the runtime Rewrote the process-substitution loops as here-strings
First managed GhidraSQL run glibc aborted in pthread_mutex_lock Two cpp-httplib revisions with different feature macros linked into one executable — incompatible inline class layouts Forced a single shared cpp-httplib source and linked it publicly
First strict source build Fallthrough and "control reaches end" diagnostics became errors The compiler didn’t know usage() and the OOM handler never return Marked both _Noreturn; final strict build is clean

Look at row six. Ghidra’s own launcher uses Bash process substitution — done < <(command) — which needs /dev/fd. The VM doesn’t have it. The fix is twelve lines:

 while IFS=$'\r\n' read -r line; do
+	[ -z "$line" ] && continue
 	IFS='=' read -r key value <<< "$line"
 	if [ -z ${!key} ]; then
 		export $key=$value
 	fi
-done < <("${JAVA_CMD}" -cp "${LS_CPATH}" LaunchSupport "${INSTALL_DIR}" -envvars)
+done <<< "$("${JAVA_CMD}" -cp "${LS_CPATH}" LaunchSupport "${INSTALL_DIR}" -envvars)"

Same semantics, no /dev/fd, plus a guard for the empty line that the here-string form introduces. That’s not pattern-matching a Stack Overflow answer; that’s understanding why the construct failed.

And row seven is the one that made me sit up. The managed server crashed inside pthread_mutex_lock — the kind of bug that eats an afternoon. It correctly identified an ODR/ABI mismatch: two different revisions of the header-only cpp-httplib were being pulled into one executable with different OpenSSL/zlib feature macros, so the inline class layouts disagreed and the server object’s synchronization state was corrupted. It wrote the CMake fix, rebuilt, and the server came up.

That’s a real bug in my project, found by an agent on my phone, with a correct diagnosis and a working patch. I’m keeping that fix (and its explanation) for GhidraSQL upstream rather than reprinting it here — but it’s the strongest single result of the run.

The actual reverse engineering

With GhidraSQL alive, it picked its own target: the system /bin/ls. A stripped x86-64 PIE ELF, GNU coreutils 9.4.

The choice of binary barely matters. It could have been a malware sample, a firmware blob, or a vendor library — the workflow is identical. ls is a good demo precisely because you can check the answer: everyone knows what ls is supposed to do, so a reconstruction either behaves like ls or it doesn’t.

The first query sets the scene:

SELECT * FROM binary;
SELECT COUNT(*) AS funcs, SUM(size) AS total_bytes FROM funcs;
SELECT COUNT(*) AS edges FROM callgraph_edges;
SELECT COUNT(*) AS string_hits FROM string_refs;

412 functions. 79,542 bytes of function body. 1,363 call graph edges. 259 string references. Stripped, so every one of those functions is named FUN_00104da0 and typed undefined.

Finding main in a stripped PIE binary is the classic first move, and it did it the classic way — one query:

SELECT text FROM pseudocode WHERE func_addr = 0x106D30;
void processEntry entry(undefined8 param_1, undefined8 param_2)
{
  undefined1 auStack_8 [8];
  __libc_start_main(FUN_00104da0, param_2, &stack0x00000008, 0, 0, param_1, auStack_8);
  do { /* WARNING: Do nothing block with infinite loop */ } while( true );
}

The first argument to __libc_start_main is main. There it is: 0x104DA0. From there it worked outward through the call graph, string evidence, and scoped decompilation — 8,029 bytes of main, 72 distinct callees, GNU help text and embedded src/ls.c assertion strings as corroboration.

Then it started writing findings back. Each annotation is one transaction that reads the function, tags it, renames it, sets a prototype, attaches a plate comment, reads it back to verify, and saves:

SELECT addr,name,prototype FROM funcs WHERE addr=0x107D70;
INSERT OR IGNORE INTO function_tag_mappings(func_addr,tag_name) VALUES(0x107D70,'progress:seed');
UPDATE funcs SET name='ascii_strncasecmp',
  prototype='int ascii_strncasecmp(char *left, char *right, long length)' WHERE addr=0x107D70;
INSERT INTO comments(addr,comment,source)
  VALUES(0x107D70,'Compare at most length ASCII bytes case-insensitively; locale-independent helper.','plate');
INSERT OR IGNORE INTO function_tag_mappings(func_addr,tag_name)
  VALUES(0x107D70,'progress:reviewed'),(0x107D70,'progress:annotated');
SELECT addr,name,prototype FROM funcs WHERE addr=0x107D70;
SELECT save_database();

Read → write → read-back → save. Fifteen times. The discipline was deliberate and recorded: keep decompiler queries scoped to one function, serialize decompiler work, establish names from multiple evidence sources before writing, verify every mutation, save after coherent groups.

Here’s what came out:

Address Recovered name Prototype Role
0x104DA0 main int main(int argc, char **argv) Initialize state, parse options, gather/sort/print operands, drain the directory queue
0x106EA0 get_funky_string bool get_funky_string(char **dest, char **src, int equals_end, long *length) Decode LS_COLORS escape syntax
0x107D70 ascii_strncasecmp int ascii_strncasecmp(char *left, char *right, long length) Locale-independent bounded ASCII comparison
0x1090E0 usage void usage(int status) Print usage/help and exit
0x109EE0 stdout_isatty bool stdout_isatty(void) Cache and return stdout terminal state
0x109F10 clear_files void clear_files(void) Free accumulated file records, reset per-listing state
0x10B910 required_statx_mask uint required_statx_mask(void) Build the metadata mask required by active options
0x10BBB0 color_indicator_is_nonempty bool color_indicator_is_nonempty(uint indicator) Test whether a selected color indicator has content
0x10BD80 get_type_indicator char get_type_indicator(char stat_ok, uint mode, int filetype) Return the classification suffix for a file type
0x10E4B0 sort_files void sort_files(void) Sort the file-pointer array using active options
0x1105F0 print_current_files void print_current_files(void) Render the current vector in the selected layout
0x110B80 queue_directory void queue_directory(char *name, char *realname, bool command_line_arg) Prepend a deferred directory traversal record
0x110C00 extract_dirs_from_files void extract_dirs_from_files(char *dirname, bool command_line_arg) Move directories from the file vector to the traversal queue
0x110FD0 print_dir void print_dir(char *name, char *realname, bool command_line_arg) Enumerate, gather, sort, print, recurse
0x1174A0 gobble_file ulong gobble_file(char *name, uint filetype, bool command_line_arg, char *dirname) Gather one file record and its requested metadata

Anyone who has read coreutils will recognize those names. They weren’t looked up — they were derived, then independently corroborated by the embedded assertion strings.

Proving it stuck

Naming functions in a decompiler is easy. Making the work persist is where headless automation usually falls apart. So it tested itself: final save_database(), clean shutdown, then reopen the same project read-only and ask whether anything survived.

SELECT program_revision() AS reopened_revision;
SELECT f.name, printf('0x%X',f.addr) AS addr, f.prototype, c.comment
  FROM funcs f JOIN comments c ON c.addr=f.addr
  WHERE f.addr IN (0x104DA0,0x106EA0,0x107D70, /* …all 15… */ 0x1174A0)
  ORDER BY f.addr;
SELECT COUNT(DISTINCT func_addr) AS persisted_annotations
  FROM function_tag_mappings WHERE tag_name='progress:annotated';

All fifteen came back, prototypes and plate comments intact:

main · 0x104DA0 · int main(int argc, char **argv) · "Initialize locale and global listing defaults; parse short/long options and environment; configure quoting, color, time, and layout; gather command-line operands; sort/print files; then drain the directory traversal queue."

readonly server exit=0. The live database had reached program revision 316 before its final save.

The payoff: it compiles, and it’s right

From the recovered architecture it wrote a C reconstruction — recovered_ls.c, ~28 KB — preserving the six-stage structure it had recovered: parse options → gather into a file vector → sort → render → extract directories into a deferred queue → enumerate and recurse.

It builds clean under warnings-as-errors:

cc -O2 -std=c11 -Wall -Wextra -Wpedantic -Werror -o recovered-ls recovered_ls.c

No FUN_, no DAT_, no LAB_, no undefined8, no decompiler warning comments. Then the test that actually matters — run the real ls and the reconstruction over the same directory and diff:

$ /bin/ls -1A --color=never        $ ./recovered-ls -1A --color=never
Makefile                            Makefile
README.md                           README.md
recovered-ls                        recovered-ls
recovered_ls.c                      recovered_ls.c

The diff is empty. Identical names, identical order.

And it checksummed everything it produced — 26 files, sha256sum -c clean, plus a cmp proving the analyzed binary matched the system one byte for byte.

The closing summary and artifact downloads

The end of the session: every artifact offered as a download. This is the "ask for a zip" ending that makes the whole loop portable.

What this is not

The reconstruction is a behavioral one, not GNU’s source, and it doesn’t pretend otherwise. It deliberately leaves out full quoting styles, complete LS_COLORS handling, ACL/SELinux columns, dired offsets, hyperlinks, locale-aware display widths, version sorting, and most long-option aliases. Each of those would need substantially more type, global, and library recovery.

The raw Ghidra pseudocode stayed in the query transcript; the reconstruction itself contains none of it. It’s shipped under GPLv3 to match the analyzed original.

Provenance was recorded rather than asserted: GhidraSQL at 33d5dea, libghidra at 0afcaa3, ghidrasql-skills at 73a3275, all v0.0.3, all cloned with plain git — no GitHub connector, no GitHub-specific skill, exactly as I asked.

Do this yourself

The pattern is simple enough to dictate while walking:

  1. Start with the shell. "Start with a terminal shell, then git clone …" — it grounds everything that follows in a real machine.
  2. Point at docs, not steps. "It has an install markdown, follow it." Let it read the runbook.
  3. Pre-authorize the mess. "Catch all the errors. Figure it out." Without this it asks permission and stalls; with it, it debugs.
  4. Leave room for judgment. "Pick a binary of your choice." Constraints you don’t need are just opportunities for it to guess wrong.
  5. Ask for the zip. Everything it built comes home with you.

Total wall time: 52m 26s to get from git clone to a working headless GhidraSQL, 22m 41s for the recovery itself. I was not at a computer for any of it.

That’s the thing worth sitting with. The bottleneck in this session wasn’t the model’s reasoning — it was 930 MB of downloads and a serial rebuild. The reverse engineering was the easy part. And the whole thing ran on a machine I’ll never see again, kicked off from a phone, on a walk.


In this folder: report.md, the agent’s own write-up — findings, workflow, every failure and fix, and the reconstruction’s limits — plus recovered_ls.c and a Makefile. Build it with make, then try ./recovered-ls -la ..

The analyzed binary is not redistributed; it was the stock /bin/ls from the VM, SHA-256 0148f5ab3062a905281d8deb9645363da5131011c9e7b6dcaa38b504e41b68ea.

See also: Full system inventory of the ChatGPT Work VM — what’s actually inside the machine · GhidraSQL — the SQL interface to Ghidra used here.


Appendix: Headless GhidraSQL recovery report

Target and environment

The target was the local /bin/ls, copied byte-for-byte before analysis.

Property Value
Format ELF 64-bit LSB PIE executable
Architecture x86-64, little-endian
State Dynamically linked, stripped
GNU version coreutils 9.4
Entry point 0x106D30
Recovered main 0x104DA0
SHA-256 0148f5ab3062a905281d8deb9645363da5131011c9e7b6dcaa38b504e41b68ea

The completed stack used Ghidra 12.1.2, GhidraSQL 0.0.3, libghidra 0.0.3, OpenJDK 21.0.12, Gradle 8.14.3, and CMake 3.31.8. The reconstruction was validated with Ubuntu GCC 13.3.0.

Repository revisions:

  • GhidraSQL: 33d5dea43accfe67f4e17a6c7e63e44c053ee47e
  • libghidra: 0afcaa349c7b5d8f60673b64d2093e3f70ef67d7
  • ghidrasql-skills: 73a32759488e6ff208796bdbe59a0d24d7a6cb99

Live analysis workflow

GhidraSQL was launched in managed headless mode with an ephemeral local libghidra RPC port, SQL HTTP service on 127.0.0.1:8081, a persistent Ghidra project, and shutdown policy save.

The recorded query sequence was:

  1. Inventory the binary, functions, call graph, imports, strings, and hot functions.
  2. Trace _start through __libc_start_main to recover main.
  3. Scope decompiler queries by function address and use call/xref/string evidence to infer responsibilities.
  4. Rename, prototype, plate-comment, and progress-tag 15 high-value functions.
  5. Read each target before mutation, verify it after mutation, and call save_database().
  6. Query the final live revision and annotation count, save, and stop the managed server.
  7. Reopen the same project read-only and query every recovered function, prototype, comment, and tag to prove persistence.

The live database reached program revision 316 before its final save. On read-only reopen, all 15 annotations were present. The different reopened revision representation (3) is the persisted project database’s revision, not a loss of annotations.

Recovered structure

The recovered architecture is:

  1. Parse options, locale, environment, terminal state, quoting, color, time, and output layout.
  2. Gather operands and directory entries into an owned file vector.
  3. Sort the vector using the selected sort mode and direction.
  4. Render the current vector in long, column, row, comma, or single-column form.
  5. Extract directories into a deferred queue.
  6. Enumerate queued directories and optionally enqueue recursive children.

The strongest evidence came from the recovered main, the call graph, embedded GNU help text and src/ls.c assertion strings, and individually scoped decompilation of the gather/sort/render/traversal functions.

Installation and runtime failures

Stage Observed failure Cause Resolution
Toolchain discovery Required JDK 21, modern CMake, and Gradle were absent or unusable from the system path Host tools did not satisfy the install prompt Installed workspace-local OpenJDK 21.0.12, CMake 3.31.8, and Gradle 8.14.3
Java loader libjli.so: cannot open shared object file The relocatable JDK’s private libraries were not on the loader path Supplied the JDK lib and lib/server directories through LD_LIBRARY_PATH
Temporary files Java/native steps could not rely on a normal /tmp The runtime had no usable conventional temporary directory Used workspace .tmp through TMPDIR and -Djava.io.tmpdir
Dependency transport HTTPS dependency resolution/downloads failed or were interrupted The environment used restricted/proxied transport and Java trust did not initially match it Used the available Git transport, resumable downloads, and a workspace-local Java trust setup
Parallel native build The assembler received truncated input during a concurrent build Resource pressure interrupted a compiler process and left an incomplete assembly stream Removed only the failed target output and rebuilt serially
Ghidra launch support/launch.sh failed where Bash process substitution required /dev/fd /dev/fd was unavailable in the runtime Replaced the two process-substitution loops with command-substitution here-strings and empty-line guards
First managed GhidraSQL run glibc aborted in pthread_mutex_lock Two cpp-httplib revisions/configurations were linked into one executable: fastmcpp’s cpp_httplib and the top-level httplib, with different OpenSSL/zlib feature macros and therefore incompatible inline class layouts Forced fastmcpp’s FetchContent declaration to reuse the top-level cpp-httplib source and linked fastmcpp_core publicly to httplib::httplib
First strict recovered-source build Fallthrough and “control reaches end” diagnostics became errors The compiler did not know that usage() and the out-of-memory handler never return Marked both helpers _Noreturn; the final strict build is clean

The CMake fix is the material upstream source change. The Ghidra launch patch is an environment-specific compatibility patch.

A non-fatal Ghidra FileStore warning appeared for /var/tmp/root-ghidra/fscache2. It did not prevent project creation, analysis, saves, clean shutdown, or read-only reopen.

Validation

  • Exact target copy: source and captured binary SHA-256 hashes match.
  • Headless live project: 412 functions, 79,542 bytes in functions, 1,363 callgraph edges, 259 string references.
  • Mutations: 15 functions named, prototyped, commented, and tagged.
  • Save: final save_database() returned 1.
  • Shutdown: managed server exited 0.
  • Persistence: read-only reopen returned all 15 annotations; readonly server exited 0.
  • Source cleanliness: no FUN_, DAT_, LAB_, decompiler undefinedN types, or decompiler warning comments occur in recovered_ls.c.
  • Strict source build: C11 with -Wall -Wextra -Wpedantic -Werror succeeded.
  • Behavioral smoke test: --version, numeric long listing, and single-column directory listing succeeded.
  • Deterministic comparison: system /bin/ls -1A --color=never and recovered-ls -1A --color=never produced identical names and order for the source directory; the diff was empty.

Reconstruction limits

recovered_ls.c is a clean behavioral reconstruction, not a claim to the original GNU implementation. It intentionally leaves out advanced GNU features whose exact reimplementation would require substantially more type, global, locale, and library recovery: full quoting styles, complete LS_COLORS, ACL/SELinux columns, dired offsets, hyperlinks, locale-aware display widths, version sorting, and every option alias.

Raw Ghidra pseudocode was retained in the query transcript for auditability. The recovered source itself contains none of those decompiler artifacts.


Originally published in TheBigPromptLibrary on GitHub (view source).
⭐ Star the library on GitHub for more system prompts and research.

Your Own Key, Their Telemetry: Reverse-Engineering What Claude Code Phones Home in BYOK Mode

You paid for your own API key. You pointed Claude Code at your own provider. You assumed the conversation was between you and your endpoint. It isn’t — not entirely. Here’s what still flies back to Anthropic, captured off the wire, and the one line of config that shuts it up.

Point-in-time snapshot — capture performed 2026-02-04. Claude Code changes fast; the client version behind this capture is a moment in time, and some internals below have already shifted (e.g. the flag backend moved from Statsig to GrowthBook). Treat this as a dated field report, not a description of current behavior. The [DOC] claims are re-verifiable against live docs; the [WIRE] observations reflect what the client did on 2026-02-04.

Ethics & method. Everything here comes from watching my own machine’s HTTPS traffic with mitmproxy — the same lawful, on-your-own-device inspection any developer can run. Nothing was exfiltrated, decrypted from anyone else, or accessed without authorization. This is transparency research, in the same spirit as my RECon 2024 GPT-reversing work. I flag every claim as [DOC] (Anthropic-documented), [WIRE] (observed in my own 2026-02-04 capture), or [FLAG] (community-observed internal detail, not guaranteed).

The uncomfortable one-liner

When you run Claude Code in Bring-Your-Own-Key mode:

export ANTHROPIC_BASE_URL="https://your-provider.example/v1"
export ANTHROPIC_AUTH_TOKEN="your_api_key"

…your prompts and code go to your provider. Good. But by default, Claude Code still opens side-channels to Anthropic-controlled infrastructure and streams operational telemetry, error traces, and feature-flag lookups — while computing an estimated dollar cost for API calls it isn’t even billing you for. [DOC][WIRE]

None of it contains your source code. All of it is enough to fingerprint you, time you, and profile how you work.

And here’s the part most "privacy" write-ups miss: custom-base-URL BYOK is treated like the direct Anthropic API — telemetry defaults ON. Bedrock, Vertex, and Foundry users get it off by default. Roll your own gateway, and you inherit the chattier posture. [DOC]

What I actually saw on the wire

Capture setup: mitmproxy, HTTPS interception, cert pinning disabled, one trivial task — "create a C hello world, build it with CMake, run it." One boring task. Look how much came back.

Destination Relative volume What it’s for
Anthropic telemetry / log intake (Datadog) many Usage metrics, timing, cost
Feature-flag service several Runtime flag evaluation (tengu_*)
Error-reporting endpoint a few Crash / error stack traces
Your custom provider — The only thing you thought was happening

They know your machine — persistently

Every session carries a stable device fingerprint plus a per-session ID: [WIRE]

// [WIRE] observed shape
{ "device_id": "50xx… (persistent SHA-256)", "session_id": "xxxxxxxx-xxxx-…" }

The session_id rotates. The device_id does not. It’s a durable hash that ties every session you ever run — across projects, across providers, across months — back to the same box. On your own key, with your own provider, you are still a stable, trackable identity to Anthropic. That’s the finding.

They know how you work (not what you write)

The telemetry is metadata-dense and content-free — sizes, types, durations, verdicts. A representative slice of what streams out during a session: [WIRE]

  • Every tool call: name, duration, success/failure, result size — Bash, file writes, todo updates, the lot.
  • Bash commands, classified: not the string, but the type — mkdir, cmake, git, cd — plus exit code and output byte-length.
  • File operations: create/write/delete, hashed path, lines added/removed, char count.
  • Token + cost: input/output/cache tokens and a computed cost_usd — estimated on their side even though your provider is doing the billing. [DOC][WIRE]
  • Timing: time-to-first-token and total duration per call — a latency fingerprint of your provider.
  • BYOK tell: your base_url and model selection can ride along in request telemetry — i.e. Anthropic can see which alternative provider you defected to. [WIRE]

Illustrative shape (structure, not a guaranteed schema):

// ILLUSTRATIVE reconstruction of an observed event
{
  "event": "tengu_tool_use",
  "properties": { "tool_name": "Bash", "duration_ms": 2531, "success": true, "result_length_chars": 45 }
}

Stitch these together and you don’t get my code — you get a behavioral cast of my session: how many tools, how fast, how often I failed, how big my outputs were, which provider I used, and what it cost. Anonymous it is not, given that persistent device_id.

The full event zoo

A single session lit up dozens of distinct tengu_* event types [WIRE][FLAG] — session lifecycle (tengu_session_started / _ended, tengu_request_initialized), API (tengu_api_request, tengu_api_usage, streaming events), tools (tengu_tool_use, tool-result and rate-limit events), bash (tengu_bash_tool_command_executed, tengu_bash_tool_timeout), files (tengu_file_tool_applied, read events), conversation, errors and retries.

"Tengu" — the client’s own nervous system

[FLAG] tengu_ is Claude Code’s internal codename, wired into its runtime feature-flag system (originally Statsig; migrated to GrowthBook after Statsig’s Sept-2025 acquisition). None of it is officially documented; all of it is visible if you watch the client evaluate flags on startup. Community-observed gate names are the spicy part — they read like a changelog of things the client is quietly deciding about you:

  • tengu_attribution_header — request attribution tagging
  • tengu_anti_distill_fake_tool_injection — a countermeasure whose very name suggests anti-distillation / anti-scraping defenses baked into the client
  • tengu_penguins_off — an internal kill-switch of some flavor

You don’t have to speculate about intent to find it notable that your local CLI ships remote-controlled behavior flags with names like "anti-distill fake tool injection." These names are community-observed rather than documented, and they churn between releases — hence the [FLAG] marker on this section.

The redemption arc: it’s all opt-out

Here’s what keeps this fair — and, honestly, more useful than pure outrage. None of this is mandatory. Anthropic documents the switches, and a single one silences telemetry, error reporting, auto-update, feature-flag lookups, and model discovery in one shot: [DOC]

export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1   # the wire goes quiet

Set it and the Datadog / flag / error chatter drops away, leaving only your provider on the wire. The full, tiered hardening config — plus the two settings people think belong here but don’t — is in Lock it down at the end.

What’s private vs. what leaks (default BYOK)

Stays with you / your provider only [DOC][WIRE]

  • Prompts and model responses · file contents and diffs · full command strings (only a coarse type leaks) · real file paths (only hashes leak)

Anthropic’s own docs back the content-exclusion, for the record: [DOC]

"Metrics never include your code, prompts, or file paths."

Leaves to Anthropic-side infra by default [WIRE]

  • Persistent device_id + session IDs · tool-usage metadata · bash command types + exit codes · token counts and estimated cost · TTFT/total timing · your provider base URL + model · redacted error traces

Bottom line

  1. BYOK protects your content, not your metadata. Your code is safe; your behavior and identity are not, by default.
  2. The provider leak is the sharpest finding for BYOK users — Anthropic can see which alternative endpoint you’re routing to.
  3. You are persistently fingerprinted via a non-rotating device_id, even on your own key.
  4. It’s all opt-out — one documented env var silences it. Any critique that omits this is doing outrage, not research.
  5. Trust your capture, not the codenames. The tengu_* internals are undocumented and mutate between releases; publish what your own wire shows.

Lock it down: the hardened BYOK config

Everything above is opt-out. Here’s the config to run. Two equivalent forms — the env block in ~/.claude/settings.json, or plain shell exports (settings.json simply injects these as environment variables). [DOC]

The 80/20, if you read nothing else:

export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1   # the master kill-switch

That one flag already covers telemetry, error reporting, auto-update, feature flags, and model discovery. Everything below is belt-and-suspenders — explicit, self-documenting, and resilient if the master flag’s coverage ever changes.

Tier 1 — Privacy: stop the phone-home

Variable Effect
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC Master switch — telemetry, error reporting, auto-update, feature flags, model discovery.
DISABLE_TELEMETRY Usage / analytics metrics (Datadog).
DISABLE_ERROR_REPORTING Error / stack-trace uploads.
DISABLE_AUTOUPDATER Update-check network calls.
DISABLE_BUG_COMMAND /bug uploads — which do include content.
DISABLE_NON_ESSENTIAL_MODEL_CALLS Background model calls (auto-generated titles, etc.).

Tier 2 — Quality of life (noise, not telemetry)

These quiet the UI; they don’t change what leaves your machine. Bundled in for a clean, distraction-free setup — just don’t mistake them for privacy controls.

Variable Effect
DISABLE_COST_WARNINGS Suppresses cost / usage nag prompts.
DISABLE_PROMOTIONAL_MESSAGES No marketing / upsell messages.
CLAUDE_CODE_DISABLE_TERMINAL_TITLE Stops Claude Code from rewriting your terminal title.

Drop-in ~/.claude/settings.json

{
  "env": {
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
    "DISABLE_TELEMETRY": "1",
    "DISABLE_ERROR_REPORTING": "1",
    "DISABLE_AUTOUPDATER": "1",
    "DISABLE_BUG_COMMAND": "1",
    "DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1",

    "DISABLE_COST_WARNINGS": "1",
    "DISABLE_PROMOTIONAL_MESSAGES": "1",
    "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
  }
}

Same thing as shell exports (no file)

bash / zsh:

# Tier 1 — privacy
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export DISABLE_TELEMETRY=1
export DISABLE_ERROR_REPORTING=1
export DISABLE_AUTOUPDATER=1
export DISABLE_BUG_COMMAND=1
export DISABLE_NON_ESSENTIAL_MODEL_CALLS=1
# Tier 2 — quality of life
export DISABLE_COST_WARNINGS=1
export DISABLE_PROMOTIONAL_MESSAGES=1
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1

PowerShell:

$env:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"
$env:DISABLE_TELEMETRY                        = "1"
$env:DISABLE_ERROR_REPORTING                  = "1"
$env:DISABLE_AUTOUPDATER                      = "1"
$env:DISABLE_BUG_COMMAND                      = "1"
$env:DISABLE_NON_ESSENTIAL_MODEL_CALLS        = "1"
$env:DISABLE_COST_WARNINGS                    = "1"
$env:DISABLE_PROMOTIONAL_MESSAGES             = "1"
$env:CLAUDE_CODE_DISABLE_TERMINAL_TITLE       = "1"

Toggle-on-presence gotcha. For these flags, any non-empty value — including "0" — turns the disable ON. To re-enable a feature, remove the variable entirely; setting it to "0" will not do what you’d expect.

Tier 3 — Handle with care (you’ll see these in the wild)

Two settings get copy-pasted into "privacy" configs where they don’t belong. Listed here explicitly so you know why they’re not in the block above — not dropped, just quarantined:

  • CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 — this is an enable, and it adds traffic: the client queries your gateway to discover available models. It directly opposes CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, which switches discovery off. Only set it if your BYOK gateway genuinely needs runtime model discovery to function — otherwise leave it out for a quieter wire.
  • DISABLE_PROMPT_CACHING=1 — not a telemetry or privacy control. It turns off prompt caching, which just makes sessions slower and pricier; in BYOK the caching happens at your provider anyway, so it buys you zero privacy from Anthropic. Set it only for a specific reason (e.g. a provider that mishandles cache headers).

Sources for the [DOC] claims: Data usage · Environment variables · Settings · Network configuration · Security.

Lawful, on-device transparency research — a dated snapshot from 2026-02-04. Some internals will have changed since; re-verify against live docs before relying on the specifics.


Originally published in TheBigPromptLibrary on GitHub (view source).
⭐ Star the library on GitHub for more system prompts and research.

A Tale of Reverse Engineering 1001 GPTs: The good, the bad And the ugly

Abstract

In this talk we go deep down into the world of OpenAI’s GPTs: how they are made, what they contain, how to reverse engineer them back to their source code and exfiltrate all their "secrets" and accompanying files. This talk will take you on a fun journey into the mind of GPT writers and explore all the curious, smart and silly things they have been coding into their GPTs. Additionally, I disclose a privacy issues where custom GPTs can now collect user IP addresses and profile their users.

Description

Back in November 6th, 2023, OpenAI unveiled "GPTs", which are a custom version of ChatGPT that one can create for a specific purpose. A week later, I started releasing a series of GPTs (ask_ida IDAPython GPT, ask_ida C++, etc.), and I was curious if there’s a way to protect my GPT instructions (source code) and their knowledge files. Unfortunately, it did not take me too long to realize that protecting GPTs is a futile process at the time being. Shortly after, I embarked on a journey where I have reverse engineered and studied more than 1000 GPTs (see TheBigPromptLibrary on GitHub) and learned a lot about their internals. I will be sharing my reverse engineering insights in this talk as well as sharing around 35+ GPT anti-reverse engineering techniques.

You would be surprised what you can find when researching and reversing hundreds of GPTs:

  • Taunting messages trying to scare you away from reversing GPTs
  • Creative LLM anti GPT-jailbreak techniques
  • Pirated ebooks and rare documents
  • Secrets and API keys
  • The list goes on

Please note that this research was conducted with a strong commitment to ethics and education. All insights and techniques shared are for educational purposes, aiming to strengthen AI security and transparency. I advocate for responsible exploration and the ethical use of our findings to advance the field, not to exploit it.


Originally published in TheBigPromptLibrary on GitHub (view source).
⭐ Star the library on GitHub for more system prompts and research.

Building IDAPython on Windows

Introduction


If you use IDAPython a lot, I am sure you started reading its source code or felt the need to add missing functionality. In this article, we will show you how to build IDAPython on Windows.

On Linux/macOS, the process is straightforward as per the BUILDING.txt.

In this article:

Make sure you already installed the latest IDA for Windows and grabbed yourself a copy of the SDK.

Let’s get started.

 

Setting up VS 2019 Community Edition


IDA SDK’s default configuration is targeted towards VS 2019. Just to keep this article simple, let’s stick with that.

Grab VS2019 from here https://visualstudio.microsoft.com/vs/older-downloads/

When you install it, make sure you leave all the default installation location (in the C:\Program Files (x86), etc.)

 

Setting up Cygwin 64


Download Cygwin 64 and install the following components:

  • make from the Devel category.
  • unzip from the Archive category.

 

Setting up Python 3.x


Download Python 3.x AMD64 (say 3.4 to 3.11) and select custom install:

  • Install to C:(X = major, Y = minor)
  • Make sure you select the following options:
    • for all users
    • pip
    • Precompile standard library
    • Optional: “Download debugging symbols”

After Python is installed, install the six module:

C:\PythonXY\Scripts\pip install six

 

Setting up SWIG


In this section, we will build SWIG for Windows from scratch. We need a special patched version of SWIG (version 4.0.1, with support for -py3-limited-api) and we cannot use the pre-built binaries.

Whether you are using a Linux distro (for example Ubuntu 20) or WSL on Windows 10/11, all the steps below still apply.

Depending on your Linux setup, you may have already installed the needed packages. Just to be on the safe side, you need the following:

sudo apt update
sudo apt install wget build-essential mingw-w64 byacc bison automake autotools-dev patchelf -y

If mingw-64 failed to install:
sudo apt-add-repository ppa:mingw-w64/ppa && sudo apt-get update && sudo apt-get install mingw-w64

Clone IDAPython’s patched SWIG

git clone --branch py3-stable-abi https://github.com/idapython/swig.git swig-py3-stable-abi

Download PCRE library and build it

Download PCRE directly into SWIG’s source directory:

wget https://master.dl.sourceforge.net/project/pcre/pcre/8.10/pcre-8.10.tar.bz2

Then edit the PCRE build script in SWIG to specify the host compiler:

Open ./Tools/pcre-build.sh and change this line:

cd pcre && ./configure --prefix=$pcre_install_dir --disable-shared $* || bail "PCRE configure failed"

To:

cd pcre && ./configure --host=x86_64-w64-mingw32 --prefix=$pcre_install_dir --disable-shared $* || bail "PCRE configure failed"

(we only added the --host switch)

Now just run the PCRE build script again:

./Tools/pcre-build.sh

You are now ready to build SWIG.

Building SWIG

First run ./autogen.sh, then run the configure script with the --host switch, while also specifying static linking:

LDFLAGS="-static -static-libgcc -static-libstdc++" \
./configure --host=x86_64-w64-mingw32 --prefix=/tmp/swig4-win

Now you can run make and make install as usual.

Alternatively, you can download the pre-build version from here.

Copy the SWIG Windows binaries from /tmp/swig4-win to your Windows machine. Let’s put them in C:\idasdk\swig4.

 

Setting up the IDA SDK

  • Download the IDA SDK and unzip to a folder, for example c:\idasdk.
  • If you have the Decompiler installed, then copy the Decompiler headers from <ida_install>/plugins/hexrays_sdk/include/hexrays.hpp to c:\idasdk\include.

Initialize the SDK’s config files

We need to configure various configurations. Open the “Developer Command Prompt for VS 2019”.

If Cygwin was not in the path, then typing ‘make’ will cause an error. If that’s the case, just add it to the PATH:

set PATH=c:\cygwin64\bin;%PATH%

(Keep that command prompt open for the remainder of this article.)

From c:\idasdk, type the following commands:

cd c:\idasdk
set __NT__=1
set __X64__=1

Now let’s generate the various config files:

  • ida64 debug build: cmd /c "set __EA64__=1 && make env"
  • ida64 optimized build: cmd /c "set __EA64__=1 && set NDEBUG=1 && make env"
  • ida debug build: make env
  • ida optimized build: cmd /c "set NDEBUG=1 && make env

If you have done everything right, you should have these files in c:\idasdk:

  • vs19paths.cfg
  • x64_win_vc_32.cfg
  • x64_win_vc_32_opt.cfg
  • x64_win_vc_64.cfg
  • x64_win_vc_64_opt.cfg

To test if everything is okay, let’s try building the hello plugin:

cd c:\idasdk\plugins\hello
make

Last but not least, let’s put the IDA binaries in the correct place in accordance with the SDK make system. Assuming that IDA was installed in C:\Tools\IDA82:

xcopy /s c:\Tools\IDA82 c:\idasdk\bin

Now, anything we build will go to c:\idasdk\bin\[plugins|loaders|procs]:

  • plugins: for plugin binaries
  • loaders: for loader modules
  • procs: for processor modules

 

Building IDAPython


Okay, now we are ready to build IDAPython!

Clone IDAPython

Navigate to c:\idasdk\plugins and clone IDAPython there:

git clone https://github.com/idapython/src.git idapython

Building IDAPython

Now we have all the prerequisit steps completed, let’s build IDAPython:

set PYTHON_VERSION_MAJOR=X
set PYTHON_VERSION_MINOR=Y

C:\PythonXY\python.exe build.py --with-hexrays --swig-home c:\ida\swig4 --ida-install c:\idasdk\bin --debug
  • Replace X and Y with the proper values
  • If you don’t have the Decompiler then omit the --with-hexrays switch
  • Similarily, drop the --debug for optimized builds.
  • Run build.py --help for more information

Be patient, this can take between 5 and 30 minutes to complete the first time.

That’s it!


I know, this has been tedious but I hope it is helpful.

Contributions and suggestions to IDAPython. We are happy to discuss and merge your changes as applicable.

climacros – IDA productivity tool

Introduction

A few weeks ago, I proposed an IDA features to improve the CLI and add macros support. After a few email exchanges with Arnaud from Hex-Rays, we could not agree on how to best do it and still accommodate to everyone’s needs. Finally, Arnaud kindly wrote a quick IDAPython script to show me how I can intercept the CLI text before it gets executed. Please see Hack of the day #2: Command-Line Interface helpers.

Unfortunately, the QT approach did not work for me due to many reasons:

  • I don’t want the text to be replaced on the fly.
  • I don’t want the expanded text (macros) to be part of the history. Instead the macros should remain un-expanded as they get stored in the history.

The biggest issue we were discussing is how to format numbers: 0xHEXNUM, HEXNUM, #HEXNUM, …?

How many hardcoded macros should we have and what should they do?

In all cases, since IDA is highly programmable and I have access to the awesome IDA support team (thank you guys!), the ball is now in my court and all I had to do is write a plugin.

Enter ‘climacros’

I woke up one day with the solution to this dilemma. I don’t have to hardcode anything, instead just let the users define their own macros backed by Python expressions. The only thing I hardcoded was how to embed an expression in the CLI text. I opted for something simple inspired by the PHP/ASP syntax, something like ${ expression }$ (anything between those special tags is evaluated in Python).

With this basic idea, I set about implementing all the default macros that I think are useful for my daily work with WinDbg’s CLI.

This is climacros in action:

No more copy/paste in order to get the current address from the disassembly view! 🙂

climacros is written in C++ and is opensource. You can also grab the Windows binaries from here.

Future work

If this plugin is well received, I think people will ask for features like:

  • Macros profiles: to have a set of macros that one can switch between them quickly
  • Import/Export macros: to be able to transfer macros with users
  • Macro expansion using a scripting language of your choice and not just Python

Let me know!

 

QScripts – IDA Scripting productivity tool

Just a quick post to introduce QScripts. QScripts is a productivity tool that helps IDA users speed up their scripts development. The idea for QScripts sprang from my autox script that I wrote for the Batchography book.

With QScripts, you will be able to develop your scripts in your favorite text editor, save the script and see the results in IDA. Therefore, it makes sense to resize IDA and the text editor to see things side by side.

Please grab your copy from here.

Daenerys: IDA Pro and Ghidra interoperability framework

Ghidra has only been released for a short while and the RCE community started adopting it (scripts, tutorials, articles, etc.) really quick. Since Ghidra is free and open-source (coming soon™), I expect a torrent of contributions in the form of tools, plugins and scripts.

I think it is important not to create a schism between IDA users and new Ghidra users because that will not benefit the RCE community. For that reason, I think it would be cool to have a simple way to seamlessly run scripts between the two SRE frameworks.

Today I introduce the Daenerys framework that lets you run IDA scripts in Ghidra and vice versa. Since I am a long term IDA user, it is easier for me to start writing the IDA-to-Ghidra adapters first. As time goes by, I will become more familiar with Ghidra’s APIs and will be able to write the adapters that let users run Ghidra scripts seamlessly in IDA as well.

If you have watched the Game of Thrones show, you are perhaps familiar with Daenerys Targaryen (aka “The mother of dragons”). I chose her name for this project because IDA’s logo is represented by a certain medieval lady and Ghidra by a dragon (some say from the GodZilla lore / King Ghidorah). Having the Daenerys framework’s logo as a lady and a dragon standing on equal footing represents a harmonious relationship between the two.

If you are a graphics designers, I would appreciate it if you can create a logo for the framework better than the one I found using Bing.com’s public domain image search.

The Daenerys framework is still in its infancy and your contributions/suggestions are welcome.

Finally, I hope the RCE community continues to strive and becomes more accessible to everyone interested in the field.

Ghidra: A quick overview for the curious

Ghidra, is a software reverse engineering (SRE) suite of tools developed by NSA’s Research Directorate in support of the Cybersecurity mission. It was released recently and I became curious about it and wanted to check it out.

I have not researched to see if someone else did a similar overview article or not, however, I am writing this article for myself and those who don’t want to run Ghidra themselves and just want to learn a bit about it.

I know that it is unfair to compare Ghidra to IDA Pro, but I cannot help it: I am a long time user of IDA Pro and it is my only point of reference when it comes to reverse engineering tools.

This article is going to be long and will contain lots of screenshots. I just started playing with Ghidra and therefore, I might be wrong or might be presenting inaccurate or incomplete information so please excuse me ahead of time.

Table of contents

Continue reading →

Using Z3 with IDA to simplify arithmetic operations in functions

I have been meaning to learn about SMT based program analysis for a long time and recently I started learning about the topic. There are so many frameworks, articles and tutorials out there that I shall explore as time goes by.

Since I am still learning, I do not claim that the following material is rocket science or advanced by any means, instead, it is very basic and should be approachable enough by absolute beginners to Z3 (a theorem prover from Microsoft Research). All I know so far comes from reading Dennis Yurichev‘s e-book “Quick introduction into SAT/SMT solvers and symbolic execution” in addition to the Z3Py introductory material by Eric (https://github.com/ericpony/z3py-tutorial).

In last week’s blog post, I illustrated how to write a basic emulator to evaluate a function’s return value without running it. In today’s blog post, I am going to show how to convert thousands of arithmetic operations from x86 assembly code into simplified Z3 expressions. Continue reading →

Writing a simple x86 emulator with IDAPython

Often times, when I stumble upon IDAPython scripts, I notice that they are using inefficient / incorrect IDAPython APIs to disassemble or decode instructions (for instance using idc.GetMnem() or idc.GetDisasm()). Therefore, in this blog post, I am going to illustrate how to use IDA’s instruction decoding functions from IDAPython in order to write a very simple x86 emulator. The goal is to demonstrate the correct use of instruction decoding IDAPython APIs. By the end of this post, you should be able to solve similar problems using IDAPython. Continue reading →