Full system inventory of the ChatGPT Work VM

ChatGPT Work mode gives you a VM — a real Linux machine sitting behind the chat box, not a scratch pad that draws a chart and forgets it. You can write code in it, build that code, browse the web, run experiments, serve files, and keep a persistent filesystem for the length of your session.

That combination is what makes it powerful. Point it at a repository and it will clone it, build it, run the output, and iterate on experiments with you. When you are done, ask it to zip the results and it hands you a download link. A full develop-build-run-collect loop, driven in plain English.

So the obvious question is: what is the machine? How many cores, how much RAM, how much disk, which toolchains are already installed, and where are the walls? This is the full inventory — CPU, memory, storage, resource limits, installed software, Python and Node package lists, and the parts of the host the sandbox deliberately refuses to show you.

This article is a companion to the two earlier sandbox inventories in this repo, the Python package list and the Linux package list of the ChatGPT code interpreter — but that was a different, far more constrained box, and this one goes well past packages. Captured 2026-08-06 09:13:48 PDT (2026-08-06T16:13:48.374439+00:00) on a pristine workspace — nothing cloned, installed, or built beforehand. Every figure below is a live reading taken at that moment, not the model’s recollection of its own environment.

Practical capacity, up front

If you are sizing up what to run in this thing, the useful limits are:

  • 9 logical vCPUs
  • 23.55 GB total RAM, with 22.11 GB free at capture
  • 58.28 GB workspace storage available
  • No swap
  • Strong Python, Node.js, C/C++, document, spreadsheet, PDF, image, OCR, and media-processing support
  • No Docker/Podman, Go, Rust, .NET, CMake, Kubernetes, or database client tooling preinstalled

The environment suits moderately large Python/Node projects and Make-based C/C++ projects, subject to the restricted network and sandbox filesystem. Anything that expects CMake, a Go/Rust toolchain, or a container runtime will need to bring it along — or will simply not build.

The rest of this article is the evidence behind those numbers.

Important scope note

This is a restricted, KVM-backed execution environment. The figures below describe resources exposed to this workspace, not the full physical host. /proc, /sys, the mount table, the Debian package database, PCI devices, and GPU devices are hidden or incomplete. Where ordinary tools could not read those interfaces, the inventory falls back to direct operating-system syscalls and runtime APIs — which is why you will see sysinfo-style numbers instead of free/lscpu output.

Operating system

Item Value
Distribution Ubuntu 24.04.3 LTS
Codename Noble Numbat
Kernel Linux 6.18.35
Kernel build #1 SMP Mon Jul 27 18:07:50 UTC 2026
Architecture x86_64 / AMD64
Word size 64-bit
Byte order Little-endian
C library glibc 2.39
Virtualization KVM
Hostname f20c423af0c2
Workspace time zone America/Los_Angeles
Page size 4,096 bytes
Uptime at capture 44,515 seconds (~12 h 21 m 55 s)
Load averages 0.000 / 0.000 / 0.000
Process count reported by sysinfo 213

Note the container-style hostname and the ~12 hours of uptime on a completely idle box — this workspace had been sitting warm long before I asked it anything.

CPU

Item Value
Assigned processors 9 logical vCPUs
CPU affinity IDs 0 through 8
Exposed architecture generation AMD Zen 4
Native GCC target znver4
CPUID brand reading AMD EPYC 9V74 80-Core Processor
Hypervisor KVM
L1 instruction cache 32 KiB
L1 data cache 32 KiB
Cache-line size 64 bytes
L2 cache 1 MiB
L3 cache 32 MiB

The processor brand describes the virtualized host CPU class, not your allocation. This workspace has 9 vCPUs, not 80 dedicated cores. Physical sockets, physical-core count, topology, and frequency are not exposed. The fresh native compiler probe still reports the znver4 target.

The AMD EPYC 9V74 string is itself a small tell: the V series parts are Azure-specific SKUs, which is consistent with where this workload is hosted.

Exposed native instruction support includes MMX, SSE through SSE4.2, AVX, AVX2, FMA, BMI1/2, AES, PCLMUL, VAES, VPCLMULQDQ, SHA, GFNI, RDRAND, RDSEED, and multiple AVX-512 extensions: F, VL, BW, DQ, CD, VBMI, VBMI2, IFMA, VNNI, VPOPCNTDQ, BITALG, and BF16.

Memory

Values are dynamic and reflect this capture.

Item Size
Total RAM 23.55 GB
Free RAM 22.11 GB
Shared memory 108 MB
Buffer memory 78 MB
Swap total 0
Swap free 0

No swap. Whatever you run here either fits in RAM or gets killed.

Workspace storage

This is the mounted workspace filesystem capacity, not the host’s complete physical storage inventory.

Item Size
Filesystem capacity 67.05 GB
Used 5.32 GB
Available to workspace 58.28 GB
Raw free blocks 61.73 GB
  • Used portion of total capacity: approximately 7.93%.
  • Filesystem block and fragment size: 4 KiB.
  • Inodes: 4,194,304 total; 4,081,122 free.
  • Maximum filename length: 255 bytes.

Resource limits

Limit Soft Hard
Open files 16,384 16,384
Processes 7,851 7,851
Stack 8 MiB Unlimited
Core dump 0 (disabled) Unlimited ceiling

Main development software

Software Version Executable/status
Git 2.51.1 /usr/local/bin/git
Git LFS 3.4.1 /usr/bin/git-lfs
GCC 13.3.0 /usr/bin/gcc
G++ 13.3.0 /usr/bin/g++
GNU Make 4.3 /usr/bin/make
Python 3.12.13 Bundled primary runtime
pip 26.0.1 Bundled primary runtime
uv 0.11.33 Bundled primary runtime
Node.js 24.14.0 LTS ("Krypton") Bundled primary runtime
npm 11.9.0 Bundled primary runtime
pnpm 11.7.0 Bundled primary runtime
Corepack 0.34.6 Globally installed with Node
Perl 5.38.2 /usr/bin/perl
Bash 5.2.21 /usr/bin/bash
Zsh 5.9 /usr/bin/zsh
curl 8.5.0 /usr/bin/curl
wget 1.21.4 /usr/bin/wget
OpenSSH 9.6p1 /usr/bin/ssh
OpenSSL CLI 3.0.13 /usr/bin/openssl
apt 2.8.3 /usr/bin/apt
dpkg 1.22.6 /usr/bin/dpkg
jq 1.7 /usr/bin/jq
ripgrep 15.2.0 Bundled Codex path
nano Present /usr/bin/nano
rsync Present /usr/bin/rsync

The bundled Python and Node runtimes use OpenSSL 3.5.5. Python reports SQLite 3.50.4 and zlib 1.3.1. Node reports SQLite 3.51.2, V8 13.6.233.17-node.41, ICU 78.2, Unicode 17.0, libuv 1.51.0, zlib 1.3.1, and zstd 1.5.7.

Document, image, media, and OCR software

Software Version/status
FFmpeg 6.1.1
ImageMagick 6.9.12-98 Q16
Pandoc 3.1.3
Tesseract OCR 5.3.4
Poppler pdftotext 24.02.0
Ghostscript 10.02.1
LibreOfficeDev 26.8.0.0.alpha0 (bundled launcher works for version inspection)
Inkscape Executable present, but fails — libinkscape_base.so unavailable
Java Launcher present, but fails — libjli.so unavailable

Installed Python packages

51 installed Python distributions:

PyMuPDF==1.26.6
PyYAML==6.0.3
annotated-types==0.7.0
artifact_tool_v2==2.8.21
cffi==1.17.1
charset-normalizer==3.4.4
contourpy==1.3.3
cryptography==46.0.0
cycler==0.12.1
et_xmlfile==2.0.0
fonttools==4.61.1
joblib==1.5.3
kiwisolver==1.4.9
lxml==6.0.2
matplotlib==3.10.8
numpy==2.3.5
openpyxl==3.1.5
packaging==26.2
pandas==2.2.3
pdf2image==1.17.0
pdfminer.six==20251107
pdfplumber==0.11.8
pillow==12.2.0
pip==26.0.1
pycparser==2.23
pydantic==2.13.4
pydantic_core==2.46.4
pyhumps==3.8.0
pyparsing==3.3.2
pypdf==6.10.0
pypdfium2==5.3.0
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-pptx==1.0.2
pytz==2026.2
reportlab==4.4.9
scikit-learn==1.8.0
scipy==1.17.0
seaborn==0.13.2
setuptools==82.0.1
six==1.17.0
threadpoolctl==3.6.0
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2026.2
uv==0.11.33
websockets==16.0
wheel==0.47.0
xlrd==2.0.1
xlsxwriter==3.2.9
zstandard==0.25.0

Compare this with the 2024 code interpreter list: that sandbox shipped hundreds of packages including the full scientific/ML stack. This one is a lean 51 — document processing, plotting, and a minimal scikit-learn/scipy core. Note the internal artifact_tool_v2==2.8.21 distribution, which is not a public PyPI package.

Bundled Node.js libraries

Top-level runtime modules:

@napi-rs
@oai
@viz-js
docx
jpeg-js
lucide
marked
pdf-lib
pdfjs-dist
pixelmatch
playwright
pngjs
pnpm
pptxgenjs
sharp
tesseract.js

There are 117 package.json manifests across the bundled Node dependency tree. Global Node packages are corepack@0.34.6 and npm@11.9.0. The @oai scope is, again, internal tooling rather than anything published.

Software availability summary

Present and usable or discoverable on PATH:

apt, dpkg, git, git-lfs, curl, wget, rsync, ssh, scp,
gcc, g++, make, python3, pip, uv, node, npm, npx, pnpm,
perl, jq, rg, nano, ffmpeg, convert, pandoc, soffice,
tesseract, pdftotext, gs

Not found as command-line executables:

rpm, apk, snap, flatpak, gh, svn, hg, gfortran, clang,
clang++, cmake, ninja, meson, autoconf, automake, yarn,
bun, deno, ruby, gem, javac, go, rustc, cargo, dotnet,
php, R, sqlite3, psql, mysql, redis-cli, docker, podman,
buildah, kubectl, helm, terraform, ansible, yq, fd, vim,
nvim, emacs, tmux, screen, magick, chromium,
chromium-browser, google-chrome, playwright CLI, qpdf,
nvidia-smi, rocminfo, lspci, lsusb

Although a playwright Node module is bundled, there is no standalone playwright executable on the inspected PATH.

Inventory counts

Category Count
Unique executable files found across PATH directories 1,023
Resolvable shell command names 1,393
Shared libraries in the linker cache 505
Python distributions 51
Node package manifests in bundled dependency tree 117

Visibility limitations

This is the interesting half of the exercise — what the sandbox won’t tell you:

  • The Debian package manager executables exist, but the sandbox does not expose a complete package database. An authoritative full apt/dpkg package list cannot be generated here — which is exactly the trick that worked on the 2024 code interpreter sandbox and no longer works on this one.
  • /proc and /sys are not normally mounted for shell tools, so lscpu, free, df, lsblk, and similar utilities cannot produce their usual reports. Direct syscall/runtime readings were used instead.
  • GPU presence cannot be confirmed. Neither NVIDIA nor AMD GPU utilities are installed, and PCI/sysfs discovery is unavailable. Read this as GPU unknown, not definitively "no GPU."
  • Physical disk devices, storage model, RAID layout, CPU sockets, physical-core topology, thermal state, and clock frequency are not exposed.
  • Installed executables are not guaranteed to be functional. Java and Inkscape are both present launchers with missing runtime libraries.

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

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.