← Back to all posts
Published on · by Renaud Deraison

Logging in was enough

A developer clearing disk space found 700 MB in a hidden directory and pulled on the thread: Z.ai's ZCode had been tarring up his entire working tree, 42,411 files with 86.6% of them .git, encrypting it with a key only Z.ai holds, and posting it straight to an Alibaba Cloud bucket. No attacker, no CVE, no malware. The trigger was being logged in. The client encrypted the archive, so no scanner could read it, and the only facts left were the destination and the size. Those are two of the fields Bromure Agentic Coding writes down for every request a workspace makes, on a wire the tool inside the VM does not get to choose.

Your coding tool does not need to be compromised to send your source code somewhere. It only needs a default. Trusting the vendor is not the question. Ask whether you would find out.

You are clearing space on a full disk. A hidden directory in your home folder is sitting on more than 700 megabytes, and you do not remember installing anything that size. Inside it, under a folder called checkpoints, there is a single 313 MB file with an .enc extension and a metadata record next to it that reads failureCount: 564.

Five hundred and sixty-four attempts at something you never asked for.

Seven hundred megabytes and a thread to pull

That is where the Chinese developer known as ferstar started on September 18, and the reverse-engineering write-up of what he found is worth reading in full. The directory belonged to ZCode, the desktop AI coding client from Z.ai, the company behind the GLM models. CSO Online and InfoWorld both carry his summary of the behaviour verbatim:

"Whenever you are logged in, ZCode silently packages your entire workspace — complete .git history, LFS asset cache, reflogs, and global app configs — encrypts it, and uploads it directly to Aliyun OSS."

The trigger sits in the first clause. Not a question you asked, not the indexing feature you switched on: a valid session token, which the capture process waits for and nothing else. One session in his logs recorded 62 capture events. When he deleted the archive, the client built a new one inside half an hour and incremented the failure counter.

What was in the box

The snapshot he took apart held 42,411 files, 345 MB before encryption and 313 MB after. What made up those 345 MB:

  • .git/lfs/: 196.1 MB, 56.8%
  • .git/objects/: 102.2 MB, 29.6%
  • .git/logs/: 0.6 MB, 0.2%
  • everything else, meaning actual source and documentation: 46.2 MB, 13.4%

86.6% of the upload was .git. The archive carried every object in the repository's history, every large binary the LFS cache had pulled down, and the reflog, which lists each branch you have been on and each commit you made and then undid. Anyone who reads your working tree learns what you are shipping; anyone who reads your .git learns what you tried, what you reverted, and who committed it.

One login token, four steps, no attackerYour machine, logged inworking directory.git/lfs, .git/objects, .git/logssource, docs, global app configs42,411 files · 345 MBTwo switches in Settingsexperience optimisation → training usesnapshot indexing → server-side indexneither one gates the packaging1 · zcode.z.aireturns an OSS form signatureand an RSA public keyprivate half stays on the server2 · packed and sealed locallytar.gz → AES-256-CTRkey ← RSA-OAEP-SHA256313 MB, unreadable to you3 · POST to Alibaba Cloud OSSzcode-prod bucketdirect — not via Z.ai's serversone object per capture event4 · OSS callbacktells the backend the object landedthe snapshot is registered andindexable on their sideWhat set it offA valid login token. Not a prompt, not a question, not the indexing feature.62 capture events in a single session. Delete the archive and it is rebuilt inside thirty minutes.failureCount: 564
How the archive left the machine. The ZCode client asked zcode.z.ai for upload credentials and received an OSS form signature together with an RSA public key; it tarred the working directory, encrypted the archive locally with AES-256-CTR under a key wrapped with that public key, and posted the result straight to Alibaba Cloud object storage, bypassing Z.ai's own application servers, with an OSS callback telling the backend the object had landed. The matching private key never reaches the client, so the user cannot decrypt their own archive. A valid login token is the trigger, not any use of the assistant. One session logged 62 capture events, and deleting the file caused a repack within about thirty minutes.

Both switches governed their side

ZCode shipped with two settings a careful developer would have found and turned off: one for experience optimisation, one for repository snapshot indexing. Both did something real, and neither did what its name suggests.

The first governed whether Z.ai was authorised to use the data. The second governed whether the server indexed the snapshot once it arrived. The packaging and the upload ran regardless, because the capture sidecar starts whenever the token provider hands it a valid JWT, with no user preference anywhere in the path. Turn both switches off and the client still tarred your repository, still encrypted it, still posted it to the bucket, and promised discretion about what happened after that.

One vendor's bug, but the shape repeats everywhere. A control that lives inside the program it restrains is a request. It runs on the same code that would have to be wrong for you to need it.

One snapshot: 42,411 files, 345 MB before encryption.git/lfs — 196.1 MB.git/objects — 102.2 MBsource — 46.2 MB56.8%29.6%13.4%86.6% of what left the machine was the repository's history — objects, LFS cache, reflogsThe reflog alone is the list of every branch you were on and every commit you made and then undid.Global app configuration files travelled with it.
345 MB, and only 46 of them were code. The snapshot ferstar recovered held 42,411 files: 196.1 MB of Git LFS cache, 102.2 MB of Git objects and 0.6 MB of reflogs, 86.6% of the payload, against 46.2 MB of actual source and documentation. A tool that uploads your working directory uploads everything you have ever written in that repository, including the branches you abandoned.

Nothing on the wire could read it

The client encrypted each archive with AES-256-CTR before it left the machine and wrapped the symmetric key with an RSA-OAEP-SHA256 public key handed to it by zcode.z.ai. The matching private key never comes down. ferstar could not open his own archive; the one party who could was the one holding the bucket.

That rules out every defence you own that works by reading bytes. A secret scanner finds no AWS key in that tarball, and deep packet inspection sees a TLS session to a storage endpoint carrying ciphertext wrapped in ciphertext.

Two facts survive: where it went and how big it was. A 313 MB POST to an object-storage host in a region your team does not deploy to stands out, provided somebody writes down where your tools connect and how much they send. Nobody did, which is why finding this took disk-space archaeology and a week of reverse engineering instead of a glance at a log.

Where the exit is

Bromure Agentic Coding puts the agent and its toolchain inside an Ubuntu VM on your Mac, and gives that VM exactly one way to reach the internet: a man-in-the-middle proxy running on the host, outside the guest, where no process in the VM can reconfigure it.

This asks nothing of the software. The virtual network switch transparently diverts the VM's port-80 and port-443 flows into the host proxy, with no environment variables required and nothing the guest can unset, on by default for every workspace. A tool that ignores HTTPS_PROXY, opens its own socket and talks straight to a storage endpoint still passes through it, because the interception lives in the switch the guest is plugged into rather than in the guest.

Four things then happen to a 313 MB POST nobody asked for.

Every request leaves a record

Every request through the proxy leaves a trace record: timestamp, host, port, method, path, status, latency, and request bytes measured before any credential swap. You do not need to decrypt an archive to notice a repository-sized upload to a host you have never deployed to: the size and the destination are columns. Sort by one, filter by the other.

It never leaves the Mac

The per-workspace egress firewall is an ordered rule table (action, protocol, host or CIDR, ports, HTTP methods) ending in default allow or default deny. Run a workspace allow-list style and the upload has nowhere to go. Two places enforce the rules: the switch matches destination IP and DNS-snooped hostname, the proxy matches TLS server name again, and your edits reach running sessions immediately.

Every verdict is a row

Every firewall decision lands in the Security Timeline, so a tool that keeps retrying, 564 times say, shows up as a repeating denial against a named host rather than as a slowly filling disk. The pattern that took a week to reconstruct is the thing the window is built to display.

The configs it packs are decoys

"Global app configs" is the phrase that should worry you most in ferstar's sentence. Inside a Bromure workspace, ~/.git-credentials, ~/.docker/config.json, ~/.kube/config and ~/.aws/config exist and work, and every value in them is a structure-preserving fake. No file, environment variable or process in the VM ever holds a real API key, OAuth token, AWS secret or SSH private key.

Scope does the rest. A workspace is one project, or one credential boundary; two workspaces never share a VM, and the guest's home is its own image file. Whatever a tool tars up inside a workspace is that workspace, which leaves out the other forty repositories on your Mac, your host's ~/.aws/config, and your SSH keys. The keys never reach a guest at all: signing happens on the host over a vsock bridge, and the private bytes stay there.

The tool on the MacHome directory~/work/* : every repository, every .git~/.aws/config, ~/.docker, ~/.ssh : realThe tool chooses what to read and where to send it.The tool enforces its own settings.313 MB, encrypted, to a storage bucketNo record of the destination. No record of the size.How you find outYou run out of disk, and start reverse engineering.The tool in a workspaceWorkspace VM — Ubuntu guest, one project/home/ubuntu — this repository, this checkout~/.aws/config, ~/.docker — placeholdersThe tool still chooses what to read and send.It does not choose the route.:80 and :443 diverted to the host proxyhost · method · path · request bytes → tracedeny web *.aliyuncs.com · default denyRefused at the wire, and written to the Security Timeline.
The same tool, two placements. Run it directly on the Mac and it sits beside every repository you have, every credential file in your home directory and an unmediated route to the internet; the upload is a TLS session like any other, and the archive is encrypted so nothing that reads content can help. Run it in a Bromure workspace and its port-80 and port-443 flows are diverted into a host-side proxy it cannot reconfigure: the proxy records the destination and the byte count, the egress rules decide whether the request leaves at all, and the configuration files it packaged hold placeholders whose real values never entered the VM.

What Z.ai did next

Z.ai apologised the day ferstar published. The company disabled the upload workflow in ZCode v3.14.0, which InfoWorld reports shipped on September 19 listing a fix for an abnormal repository-upload issue, and brought in the security firm NSFOCUS to audit the result. NSFOCUS confirmed that "all data objects in the zcode-prod Alibaba Cloud OSS bucket, as well as the bucket itself, have been deleted", and Z.ai says that after the audit "no functional path capable of triggering the generation of local repository snapshots or transmitting local files externally was identified", and that the code "has never been used for model training". Then they open-sourced the thing: client, backend, shared UI, Agent CLI and runtime, Apache-2.0, on GitHub. That beats the industry norm, and it means the next person who wants to check does not need a disassembler.

Cris Thomas of Semgrep, quoted in CSO's coverage, named the category: "This isn't really an AI model problem, it's an old-fashioned security architecture problem." He wants vendors to disclose what leaves the machine, where it goes and how long it is kept, "with the minimum permissions turned on by default, not the maximum." No customer can enforce that from the outside.

The default is the threat model

Nobody attacked anyone here. Nobody stole a credential, poisoned a package or talked a model into anything. A product did what its authors built it to do, and what they built it to do was ship your repository somewhere you could not read it. The incident is a default, and people whose interests run adjacent to yours pick the defaults.

That is the argument for putting the boundary outside the tool. You do not have to believe your coding assistant is hostile; you only have to notice that you cannot audit what you cannot see, and that the vendor's own settings pane is an odd place to keep the switch. Run the agent and its toolchain where every connection they open crosses a wire you own, where the record of that crossing is written on your Mac, and where the rule that stops a 313 MB upload is a line you typed rather than a checkbox somebody else honoured.

Then "what is my coding tool sending?" is a window you open on a Tuesday afternoon, not a research project.