Filesystem tools
The four editor primitives a coding agent works through, the two directory calls that say what is there, and the search that finds where to point them. Each capability is its own, so a study can withhold or reconfigure one without disturbing the others.
| Capability | Tool | What it does |
|---|---|---|
read-file | read_file | Read a file, under the read mode below. |
write-file | write_file | Create or overwrite a whole file (parents created). |
edit-file | edit_file | Replace one exact, unique occurrence of a string. |
list-dir | list_dir | List a directory’s entries (directories suffixed /). |
list-dir | tree | Render a depth-bounded tree of a directory, under the ignore rule in trees below. |
search | search | Content search over the workspace, under the ignore rule in searching below. |
All five capabilities are enabled in a fresh configuration, and they appear as
their own Filesystem group in the configuration editor.
list-dir buys two calls: one directory’s entries, and the tree beneath one.
edit_file replaces exactly one occurrence. Text that is absent and text that
occurs several times are distinct failures with distinct recoveries: the first
has to be re-read, the second disambiguated.
A path these tools accept is resolved the way any process in the container
resolves one: a relative path against gg’s working directory, and an absolute
path as itself. The working directory is the run’s workspace root, or the
agent’s own worktree when it has one. .. is
ordinary and is left for the kernel to resolve. The one refusal is an empty
path, which joined onto the working directory would name that directory itself.
The container is the boundary, so these tools reach anything in it. gg’s own
shell offloads command output to /tmp/gg-shell
and tells the agent to read it there, which a workspace-confined read_file
could not honor.
The list-dir capability’s second call renders the directory tree beneath a
root, bounded by a depth. Under
responses as code the same operation is
gg.files.tree, and it is what the
opening turn uses to open a
window on the shape of the workspace before the model’s first turn.
path roots the tree at one directory — absent, the workspace root — and a path
that does not exist is not-found. A path that is not a directory is
invalid-argument.
depth counts levels of children below the root: 1 is the root’s own entries,
2 adds their contents. It is 2 when it is left out, and never more than
10, so a larger request is answered at 10. A depth of zero is
invalid-argument.
The result is one block of text, the root itself unnamed, each level indented
two further spaces than its parent, directories suffixed /, and every level in
path order:
README.mdengine/ game.js input.js render/ (4 entries not shown)src/ main.js systems/ (7 entries not shown)A directory sitting at the depth bound is suffixed with how many entries it
holds that the walk did not enter, so the bound is stated where it was reached
and the recovery is a deeper depth or a path rooted there. A root with
nothing beneath it renders as (empty directory).
The rendering is bounded so one call cannot flood a turn: at most 1,000 lines and at most 16 KiB, whichever binds first, and a result cut by either ends with a line saying so.
The tree honors ignore files on exactly the terms searching states,
so an ignored directory is absent from it and a tree holds the agent’s own
sources rather than node_modules, build output and the run’s bookkeeping.
Searching
Section titled “Searching”The search capability’s one tool scans the workspace’s files for a query and
returns the matching lines, each with its path and 1-based line number. Under
responses as code the same operation is
gg.files.search, and the default
opening turn opens its
documentation for an agent that holds it, before the model’s first turn.
The query is a regular expression (Rust syntax — foo|bar, fn\s+update,
(?i)todo for a case-insensitive match), tried against each line on its own;
a blank query and an invalid pattern are both argument errors. path roots the
search at one directory or one file — absent, the workspace root — and a path
that does not exist is not-found. Matches arrive in path order and then line
order, each as the path, the 1-based line number and the line without its
ending.
The result is bounded so one search cannot flood a turn. limit says how many
matches come back — 50 when it is left out, and never more than 200, so a larger
request is answered with the first 200 — and a list exactly limit long may have
been cut; there is no offset, because a search is a question about where to
point the other tools rather than a way of reading a file, so the answer to a
cut list is a narrower query or path. A matching line longer than 200 characters
is cut there and annotated in place as foo (123 more chars...), and a file that
is not text (one carrying a NUL byte) is skipped rather than matched byte by
byte.
The search honors ignore files: what .gitignore, .ignore and their kin
exclude — nested files, negations and .git/info/exclude included, and .git
itself — is never scanned and never returned, in a workspace that is a
repository and in one that is not yet. Dotfiles are otherwise searched like any
other file. A match list therefore holds the agent’s sources rather than
node_modules, build output and the run’s own bookkeeping, and a file under an
ignored path is still reachable by path through every other tool on this page.
Ignoring is the search’s own rule, because a search is a question about the
project rather than about the disk.
Read modes
Section titled “Read modes”How much of a file one read_file call returns is the read-file capability’s
swappable implementation, and an enabled read-file capability names one. Capping
reads is one of the load-bearing differences between real coding harnesses: a cap
stops a single call from flooding the window and forces an agent to be deliberate
about what it looks at, while costing a round trip per page. The two modes are
the arms of that experiment.
| Mode | A call that names no limit returns |
|---|---|
unlimited | The rest of the file, from offset to its end. |
default-cap | lineCap lines, from offset. |
Both modes take offset and limit and honor them whenever they are given.
The mode decides only what a call that names no limit gets.
The lineCap param sets the window a call gets when it asks for no limit of
its own. The capability writes it whichever mode is selected, so a sweep that
varies the mode over one shared params block reads the same figure on both arms
and every launch in the sweep is judged the same way. An absent lineCap, a
value gg cannot read as a line count of one or more, and an implementation gg
does not recognize each refuse the launch, the last of them naming the two
modes. A refusal names every value in the configuration gg cannot honour exactly
as written, so one pass fixes them all.
read_file takes two paging arguments under either mode, so the agent can page
through a file it did not get at once:
offset— the 1-based line to start from, defaulting to1under either mode. An offset past the end of the file is an error naming the file’s length.limit— how many lines to return. A value that is given is always honored, verbatim, under either mode; underdefault-capa larger value than the cap is how the agent talks past it.
A windowed result — one that starts after line 1 or stops short of the file’s end, whichever mode and whichever arguments produced it — ends with a line saying what the agent is looking at and where to continue from:
[showing lines 251-500 of 1200; continue with offset: 501]A result that ran to the end of the file from an offset past line 1 still
says which lines it shows, without a continuation.
Whole-file reads under either mode
Section titled “Whole-file reads under either mode”The cap bounds a call that asked for nothing rather than every call. An agent
that asks for a limit
covering the file gets the file, byte for byte, exactly as unlimited would
have returned it, with no window note appended.
That is a property gg depends on. Autoload specifications seeds a case’s specifications whole into an agent’s opening context, and it reads them through this same tool.
Two further properties keep the arms comparable:
- A file shorter than the cap reads identically under both modes, with no window note and no paging footer. Only files big enough to be capped differ between arms, so a comparison measures the cap rather than incidental formatting.
- The tool’s schema offers
offsetandlimitunder both modes and states onlimitwhat leaving it out means: the cap underdefault-cap, the end of the file underunlimited. The tool-calling system prompt states the cap only underdefault-cap, with this run’s ownlineCapinterpolated. Under responses as code no prompt states it: the paging footer above carries the file’s length and where to continue from, on the read that was actually windowed.
A separate 256 KiB byte ceiling backstops every mode, since a file can have enormous lines, and applies to whatever the line window selected. A read truncated by it says so.
Reading images
Section titled “Reading images”A test case’s specs ship reference mockups, so read_file on a .png reads the
picture as a picture.
- The format is detected by magic number rather than by extension. PNG, JPEG, GIF, and WebP are recognized; anything else reads as the ordinary file it is.
- The image is attached to its own tool result, alongside a line of prose naming
the file, format, and size. It travels to the provider as a base64
data:URL in a multi-part message. offsetandlimitdescribe lines of text, so a picture is returned whole.- An image over 8 MiB is described rather than attached. An inline image is charged to the window at a rate no estimator can pin down, and every reference mockup a test case ships clears the limit by a wide margin.
Those four describe the read_file tool. Under responses as code the detection,
the ceiling and the vision handling are identical, and the attaching differs.
See what this costs the context window.
Models that cannot see images
Section titled “Models that cannot see images”The models a study sweeps are not uniform: some accept image input and some are text-only, and a text-only model answers an image-bearing request with a hard error. Reading a reference image must never be what discards a run, so gg resolves this in two stages.
The declared stage reads the model catalog, which records the input modalities
OpenRouter reports per model, and the launch pushes them into the run alongside
each model’s context window. A model declared without
image is never sent one: read_file describes the file instead of attaching
it, and both system prompts state up front that reading images is
unsupported, on a run that offers read_file. The tool-calling prompt carries
that line in its Reading Files section; the code prompt carries the line by
itself, under its Views heading.
The learned stage covers what the catalog does not. A model with no modality list is treated optimistically, since unknown is distinct from text-only and a harness whose point is running new models should show them a test case’s mockups. If the provider then refuses the request, gg:
- records the model as unable to see images, run-wide, so every agent bound to that model stops attaching them too;
- strips the images from the context, keeping each affected tool result’s text
and its
tool_call_idso every assistant tool call still has its matching result, plus a note telling the model where the picture went; - re-runs the same turn against the now image-free conversation.
Being wrong therefore costs one request per model per run. A model’s vision support is shown on its page in the console’s Models section, under Specs.
What this costs the context window
Section titled “What this costs the context window”A read_file result enters the window as a FileView tagged with its path. It
is its own band in the context breakdown and the
thing gg.context.evictFileView targets. An
attached image is part of the same view, so evicting the view reclaims the
picture too.
An image’s cost is estimated from its dimensions. gg reads the width and height
out of the image header and charges the tile count a picture of that size costs,
so an attached mockup weighs close to what the provider bills for it. Providers
tile differently and a header gg cannot read falls back to a charge derived from
the file’s size, so the figure remains an estimate. A
capped read puts only the window it returned into the context, so default-cap
and eviction are two answers to the same problem: one rations what enters the
window, the other reclaims it afterwards. They compose.
Under responses as code the same read costs
the window nothing: gg.files.readFile hands the bytes to the program and stops
there. What puts a file in the window is gg.views.openFile(path). It performs
the identical read, under the same line cap, the same magic-number detection and
the same 8 MiB ceiling, and it also opens a file view of the result, keyed by the
path and closable by it. What one view may carry is bounded by the
view caps, which refuse an over-cap window
rather than truncating it. gg.files.readFile gets bytes for your program;
gg.views.openFile shows a file to you.
A picture obeys that split. Under the code arm gg.files.readFile of a mockup
returns the descriptor (label, format, byte size) and detects the format the
same way, with shown: false and a reason naming gg.views.openFile as the way
to look at it. A view is the only channel into a code agent’s window. A program
that means to look at a reference mockup opens a view of it; one that wants only
its dimensions or its bytes pays nothing. How many such views may be open at
once is unbounded: what bounds a picture is its own 8 MiB ceiling, and
re-opening a path takes the image out of the copy it retires, so what a run
uploads per request follows what is open.
Reads are append-only, as everything in the window is. A second read of the same file appends a second view rather than rewriting the first, and re-reading is how an agent sees a file it has changed. See Context visibility.