Skip to main content
An agent reviews code the way a developer does: it reads what changed, looks at the code around it, checks the tests and CI, and looks things up. A webhook can start it for each new pull request. A job this small needs no VM or sandbox. It runs in a lightweight terminal inside one FastAPI or Express server:
  • Files: /work, mounted in memory for scratch files and clones, and /repo, Mirage’s repository mounted read-only with the GitHub VFS, with no clone. To keep /work, mount a disk folder, an S3 bucket or another VFS there instead.
  • Python: python3, in-process on Monty.
  • CLIs: gh calls GitHub’s API with a token the server holds, and git clones a repository into /work with no git program on the host. DeepWiki, an MCP server, is installed as a CLI named deepwiki, with one subcommand for each tool.
The commands below are what the agent runs in its terminal, in two reviews that read a repository in two ways:
  • Mount, for a large repository: a pull request in Mirage’s own repository, number 2240, which stops the ntn CLI printing its token. Mirage’s repository is over 100 MB, more than a terminal should hold, so the agent reads the few files it needs through gh and /repo, and nothing is cloned.
  • Clone, for a small one: a dependency bump of Pydantic Monty, the runtime Mirage runs python3 on, from 1.4.0 to 1.6.0. Comparing two releases needs the history but not the files, so the agent clones only the history, 24 MB, with git clone -n.

Server

This is the server from the Overview with three additions: it registers gh and git, it mounts Mirage’s repository read-only at /repo, and at start it installs the DeepWiki MCP server as a CLI. The mount is pinned to the commit the pull request merged as, so /repo holds the code as merged. To review your own repository, change owner, repo and ref, and the repo of gh. Before you start the server, set GITHUB_TOKEN to a token that can read public repositories. GitHub says how to create one. The server uses it for gh and for the mount, and the agent cannot read it from the terminal.
app.py

Usage

Create a terminal, and define run, which sends commands to it and prints what they print. It needs jq:
The commands between SH and SH in the blocks below run in the terminal on the server, not in the shell that calls run.

An MCP server as a CLI

The server installed DeepWiki at start, so every terminal has a deepwiki CLI. Each tool of an MCP server is a subcommand that takes its arguments as one JSON object, and --help prints the tool’s schema. For example:
A large answer goes to a file, and the caller reads only what it asks for:
The whole Monty wiki is about 570 KB, roughly 140,000 tokens; the caller sees a byte count and a few lines.
  • The tool list is read once, at registration, and no schema reaches the model until it runs --help.
  • The server needs DeepWiki to start; mcp_cli raises when an MCP server cannot be reached. Add an entry to MCP_SERVERS to install another one.
  • A profile can deny a tool, and a network: rule can limit the hosts a terminal reaches. See From MCP.
  • DeepWiki is a third-party service with rate limits. An MCP server that needs an API key takes it in its config, as auth: {"type": "header", ...}.

Review a pull request

First the agent reads what the pull request is and which files it touches. --json with --jq prints only the fields it asks for:
Thirteen files: the command’s handler in Python and in TypeScript, its help text in both, tests for both, the test cases the two share under integ/, and two reference pages. The agent reads the Python handler’s part of the diff. awk prints from that file’s diff --git line to the next one:
The handler used to read the token and print it. Now it reads nothing and answers with an error. That raises a second question: was ntn the only CLI that did this? /repo holds the code as merged, so the agent searches every bundled CLI for the call that reads a secret:
The search reads 207 files through GitHub’s API, so the first one takes around half a minute; files already read come from the cache. Three places remain, and none of them prints: gh builds a masked copy for gh auth status, gh builds the header it sends to GitHub for a clone, and himalaya logs in to the mail server. Last, the agent counts the test files in the change and reads the result of CI:
For background, the agent asks DeepWiki how a CLI gets its token, and sends the answer to a file:
DeepWiki answers from its own index of the repository, which can be older than the pull request. The agent treats its answer as background and takes its findings from the diff and the code. The review ends with four findings the agent can report:
  • The pull request does one thing: ntn auth token no longer reads or prints the token. It answers with an error and exit code 1.
  • The change is made in Python and in TypeScript, and it comes with tests: one test file for each, and the test cases the two share.
  • No other bundled CLI prints its secret. The three calls that still read one mask it or send it to the service it belongs to.
  • CI passed: 89 checks, with 5 skipped.
To get there the agent read about 2 KB of output, and the repository was never cloned.

Review a dependency bump

Mirage pins its Python runtime exactly, and a pull request moves pydantic-monty from 1.4.0 to 1.6.0. Before it merges, a reviewer checks whether anything Mirage uses has changed. The agent starts on Mirage’s side: /repo, still on 1.4.0, shows the pin and every name Mirage imports from Monty:
Seven names. AbstractOS matters most: Mirage subclasses it, as MontyFs, to give Monty its files. Next the agent clones Monty. -n skips the checkout, so the terminal holds 24 MB of history instead of 41 MB of history and files:
Eighty-five commits, and four files of Monty’s Python API changed. The agent saves that part of the diff, about 50 KB, to a file, and a short Python file on Monty lists the public names the release adds and removes:
The release removes nothing. Most of what it adds is a remote client and its errors, which Mirage does not use. One addition is in the file protocol Mirage implements, path_create_exclusive. The agent looks for it in Mirage, then reads it in 1.6.0:
MontyFs does not define the method, and the method is not abstract: by default it raises NotImplementedError, and Monty answers open(path, "x") with exclusive creation mode is not supported, as 1.4.0 already did. So nothing breaks, and exclusive creation stays unsupported until MontyFs adds the method. Last, the agent looks for commits that change errors or rename things:
Mirage already catches MontyRuntimeError, and its code uses neither of the renamed names. For background, the agent asks DeepWiki about the new method, into a file:
DeepWiki’s answer agrees with the code: the default refuses, and exclusive mode reports that it is not supported instead of falling back to an ordinary open. The review ends with three findings the agent can report:
  • The bump is safe to merge: 85 commits, and Monty’s Python API removes nothing.
  • The file protocol gains path_create_exclusive, with a default that refuses. MontyFs does not implement it, so open(path, "x") stays unsupported, as on 1.4.0. Adding it is a follow-up.
  • MontyRuntimeError, which Mirage catches, now also covers a result that cannot be converted to Python.
To get there the agent read about 2 KB of output. Monty’s history and the 50 KB patch stayed in the terminal. For the commands and flags each supports, see gh and git. For the mount, see the GitHub VFS.