Giving a shared revision store the repository tools an agent expects
How agents get familiar status, diff, log and publishing tools over a shared revision tree with no .git, and how those tools say where their contract differs from Git's.

On this page
In August 2026 an agent on our platform picked up a task against a private repository and started the way an experienced engineer would, with git ls-remote. The command died with fatal: could not read Username … terminal prompts disabled. Our own environment had manufactured that message by setting GIT_TERMINAL_PROMPT=0, so that Git would fail instead of hanging, and nothing the agent could read explained it, so it looked like a broken token. The agent went looking for the token, found the environment empty, which was by design, and only on its third try reached the clone tool, which worked. It then ran git rev-parse HEAD in the directory it had just been given, twice, and was told both times that this was not a Git repository. Later in the same thread, a build that depended on a second private repository sent it off to work out a local-path workaround by hand, as two earlier sessions had also done.
The clone never failed, so nothing in that thread looked like a broken tool. The agent was behaving correctly for the world it believed it was in, and a directory full of source code strongly suggests that world. The only sentences saying otherwise sat in the descriptions of the publishing tools, which an agent reads when it is about to push. The status, diff and log tools that could have answered its questions were disabled stubs, while another tool was cheerfully telling the model to run them. The plans we wrote in response set a blunt goal, no failing tries on repository work, and a method to go with it, which was to teach through the surfaces a model already reads, its tool descriptions and tool results, at the moment it needs to know.
Why the tree can’t be a checkout
The tree those agents work in lives in a shared revision store that our platform team builds and runs. We build on it and don’t change it, and the part I worked on, with agents under my direction, is the layer the agents actually touch: the repository tools and what they promise. The store’s design is the reason for almost everything in that layer. Every change gets a position in a log, history is never pruned, and each sandbox attached to the tree holds a two-way replica of it. A project keeps its coordinator, its workers and their helpers in that one tree, which can mean a hundred or more agents at once, reading each other’s work without copying it.
Git doesn’t fit inside that, and we had found this out the expensive way. The first repository model put a .git in the workspace. A token ended up in .git/config, one .git couldn’t hold several repositories, and Git’s rename-heavy writes hit I/O errors on the mounted file system. The store has no ignore rules either, so anything written into the tree, a .git included, becomes permanent history replicated to every sandbox. Isolation went no better when it was tried later. Fourteen full copies of a 490 MB tree killed a sandbox’s link to the store and took the coordinator to 26 GB of resident memory, and a clone per worker on sandbox disk produced 92 clones in a day, each one lost with its sandbox and invisible to other workers and to review.
The rule that came out of this is short: Git is the boundary, never the inside. Git data enters through a clone tool that clones on the agent’s host, in temporary space, and writes plain files under a prefix for that repository. It leaves through scoped publishing tools that run against a bare cache outside the tree. Between those two points there are only files and the store’s own history.
What a prefix has to remember
Taking .git away doesn’t take its questions away. Which repositories are here, on which branch, taken from which commit? A small registry answers them, with one record per prefix pinning that directory to its upstream URL, its branch and the exact base commit its files came from. The record sits in a support directory excluded from every repository’s file set, so it travels with the workspace and can’t turn up in anybody’s pull request. This is what lets one tree hold any number of repositories, and the base commit underlies everything Git-shaped that happens later, since you can’t say what changed until you can say what it changed from.
The base turns out to be less singular than it sounds. When an update from upstream can’t merge a file that both sides have changed, the file stays as the tree had it, because a tree other agents are building from is a bad place for conflict markers, and the registry records that this one file is still on its own older base. Nor is a workspace with three repositories a single transaction. Each prefix publishes on its own against its own base, so a change that crosses two repositories is two commits with two outcomes.
Keeping the name and saying what changed
The tools carry the names you would expect. The coordinator is told that git and gh exist only as tools of those names and that they work like the commands, and workers have git_commit, git_push, git_clone and gh_pr_create. I wanted it that way because a model arrives already knowing those verbs. These models were trained on an enormous record of people using Git, and a name they already understand does work that would otherwise fall to a paragraph of instructions.
A familiar name also makes a promise, and most of the engineering goes into the places where the promise can’t quite be kept. Repository status is the clearest case. There is no object store in the workspace to consult, so the tool works down a ladder of sources, never cloning and never running Git on the mount, and it reports which rung it reached:
| How it knows | What it tells the agent |
|---|---|
| Runs Git in scratch space against the recorded base commit | “git status against the recorded base commit (exact)” |
| Reuses an earlier exact answer while the workspace is unchanged | “last exact status, cached (workspace unchanged since)” |
| Compares names and sizes with the manifest taken at ingest | “file names and sizes only (approximate: same-size edits are missed)” |
Below those it returns a typed “unavailable” instead of a guess. If the third rung finds nothing, the tool doesn’t call the tree clean. It says: “No changes detected, but only the approximate check ran, so the working tree is not confirmed clean.” Real git status has never needed that sentence, and an agent about to report that it left a tree untouched needs it badly.
Publishing departs further from the commands it resembles. A commit is built from files named one by one, which is the only sane way to commit from a tree that holds other workers’ unfinished edits. Landing on the project’s branch pushes a fast-forward only after a reviewer has approved that exact patch digest, and the base for that prefix then moves to the new tip. The ordinary push refuses main, master, trunk and the repository’s default branch, which it confirms by asking GitHub rather than trusting what the workspace believes. The coordinator’s instructions also list what doesn’t exist at all: worktrees, stash, reset and interactive rebase, with a pointer to use a branch instead. Naming the absences is part of the contract. An agent that learns by experiment that stash isn’t there has spent a step and learned only that, while an agent told up front plans differently.
Steering and enforcement are different jobs
The August thread raised an obvious question about the shell, which was whether to stop agents running the wrong Git command at all. For remote commands we chose to annotate rather than block. A classifier that runs before the command can’t be both correct and safe: an anonymous HTTPS fetch of a public repository really does work from a sandbox, so blocking commands that carry a remote URL would block commands that succeed, and a build tool fetching a Git dependency never names Git on its command line at all. The credential failure, by contrast, is deterministic by the time the result comes back, thanks to that same GIT_TERMINAL_PROMPT=0. Appending an explanation to that stderr costs the same one step a block would, can’t wrongly stop anything and catches the build-tool case for free. It also stays out of the system prompt, where the tool definitions and the system message share one byte-stable cached prefix and any change costs a cold prefill on each deploy. A tool result lands after the last cache breakpoint, so only the agent that hits the problem pays for the lesson.
Writes into the tree are a different matter, because there the right answer is known in advance. By the autumn a guard in the shell was refusing commands that would create, mutate or publish a repository, naming the tool to use instead and, for a publishing step, the route that applies to that thread. Its parser has grown to notice what agents try once a plain git push is refused: Git named through a variable or an alias, find -exec git, Git launched from inline Python or Node, a search of the environment for a token, a write to GitHub’s API through curl. It also recognizes read probes like git status and git ls-remote, which in a tree with no .git can only report that there is no .git, and which agents had taken to running to test a brief’s claim that a token was available. And it permits what is legitimate, such as cloning a third-party repository into temporary space to build it, after resolving every path the command names to be sure none of them reaches the tree.
All of that is still guidance, and the guard’s own documentation says so. It can’t see Git started by a build tool or a command assembled at run time from data, and it doesn’t have to, because it isn’t what protects anything. Sandboxes hold no GitHub credential unless the owner opts in, and the publishing tools make their own refusals. The August plan rejected a sandbox-side GitHub MCP server on those grounds, since it would have needed a credential in the sandbox. The guard exists to save an agent a wasted step and point it at the right tool. The missing credential is what stops a determined agent from publishing, and it needs no understanding of shell syntax to do that. A design that confuses the two ends up either with a parser it has to keep perfect or with a token it believes is safe because the parser is there.
The questions Git has no words for
So far the question has been how a prefix differs from upstream. With many writers in one tree, an agent also needs to know what has been happening around it, and here the familiar vocabulary runs out. Inside one mount, two writes to a file behave like POSIX on a local disk: the last one wins and nothing merges. Between a sandbox’s replica and the authoritative copy the authority wins, and the losing bytes survive only in history. The store can’t tell agents apart, since every write from the shared process carries the same origin and workers share sandboxes, so attribution has to be recorded by our tools at the boundary and reconciled against the store’s change feed. A status against the base commit tells you what differs from the base, and in a shared tree that includes everybody’s work.
In September we added revision tools that speak the store’s language instead of Git’s. An agent can take a checkpoint, which is simply the current position in the log, and later ask what has changed since. It gets one automatically at the start of its turn, bound to the authenticated turn and captured before its tools are exposed. It can compare two versions or three, as base, its own and current. It can page through a file’s history, read older text, list the writes that were superseded between two positions and preview a restore before applying it.
The limits are part of the contract, and they follow from the same shared machinery. One process and one file system daemon serve every agent in a project, so a history scan by one agent slows all the others. The tools therefore run at most four operations at a time with a queue of 64 behind them, under a five-second deadline that includes time in the queue, and a request that expires while queued never runs later, when nobody may still want the answer. History also has to fit in a model’s context and be safe to put there, and a log that never prunes holds whatever anybody ever wrote. Old text is decoded and redacted first, a revision of up to 256 KiB can be read in pages, a larger one is refused and a binary returns only metadata. A diff takes at most 64 KiB per input and says so when a comparison goes past its limits.
The tools are just as strict about what they don’t know. A lost baseline or an interrupted capture never silently becomes a newer start of turn, and when a change listing can’t be framed unambiguously the tool refuses, leaving the agent to establish a new baseline on purpose without pretending the missing interval was inspected. Restore is where this is most visible. It refuses a preview that has gone stale, records itself as an ordinary workspace change, invalidates cached file contents and any earlier verification, and checks the result it observes. It doesn’t exclude other writers and it doesn’t retry an unknown outcome. Neither its receipt nor a write-back barrier proves that every proposed write was accepted, because a write can still lose to the authoritative copy or to another agent. That is a modest guarantee to put in front of a model, and I think it’s the correct one, since any stronger wording would be false.
Exercising these from a project created through the browser turned up a contention defect in parallel reads, which makes sense, because parallel is how agents read. After the fix, a fresh hosted coordinator ran simultaneous change, history and status reads, a comparison and a restore without errors. Binding the same tools to the workers’ sandbox replicas in production was separate work.
The names get an agent through its first few moves with no teaching at all, which is what I wanted from them, and they can’t be allowed to do more than that. The agent in that August thread knew Git perfectly well. What it lacked was any way of learning, before it had spent three tries, that the directory in front of it wasn’t what it looked like.
Chris Chabot, November 2026