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.
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.
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.
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.