TOOLING.md: tell your coding agents what's in the toolbox
A coding agent working in your terminal has no manifest of what's installed on your machine. It discovers CLI tools the same way a new hire without onboarding would: by trying them. Each miss costs a turn, and worse, a command not found early in a session teaches the agent the tool is absent for the rest of it, even when the package was there all along under a different name. The fix is cheap and pleasingly boring: install a good baseline, then generate one small file that tells every agent what is verifiably present, and keep it fresh automatically.
This post walks through the pattern: how agents actually discover tools, what the ecosystem has converged on, why the file should be generated rather than written, and how the agents role turns the whole thing into one dotpro apply.
How agents discover tools
There are exactly three channels, in decreasing reliability:
- Trained assumptions. Agents reach for near-universal tools without checking:
git,grep,find,rg,jq. For these, installation is discovery; the momentrgis onPATH, Bash invocations of it start succeeding instead of falling back to slowergrep -r. - Probing. For everything else the agent runs the command and sees what happens, or checks
which fdfirst. Every miss burns a turn, and a miss can be a false negative: Ubuntu ships fd asfdfind, so an agent that triesfdconcludes the tool is missing even though the package is installed. - Being told. Instruction files (
AGENTS.md,CLAUDE.md) are injected into every session. A tool named there, with a version and a usage hint, is used from the first turn.
Channel three is the only one you control end to end, and it is where the ecosystem has been busy.
What everyone else does
The pattern shows up everywhere once you look for it, almost always hand-maintained. A widely shared gist on speeding up Claude Code tells you to write an "Installed CLI tools" section into ~/.claude/CLAUDE.md by hand, because without it the agent "falls back to slower defaults". Trail of Bits' Claude Code config does the same at organization scale: one brew install line for the CLI layer and a global CLAUDE.md documenting the toolchains. The AGENTS.md spec, now a Linux Foundation project with tens of thousands of adopting repositories, recommends exactly this shape: a small root file with breadcrumbs to detail, what its authors call progressive disclosure. And the Nix world generates the manifest outright: flake-built environments that emit a startup-readable inventory, because flake.nix is itself a manifest the agent can read.
Two research findings sharpen the design. Studies of agent instruction files warn that stale information actively poisons context: a manifest that names a tool that has since moved sends the agent confidently down the wrong path. And LLM-generated instruction files measurably reduce task success, because they state generic things agents already know. The lesson is not "don't generate", it is "generate facts, not prose": every line should carry information the agent cannot infer, and the file must never drift from reality.
Generate it, don't write it
That is the whole idea behind TOOLING.md. Instead of a hand-written list that goes stale, the agents role scans a curated list of tools on every apply: command -v to check presence, a version probe for the number, and only the tools actually found are rendered to ~/.agents/TOOLING.md. The file cannot advertise something that is missing, and it cannot go stale for longer than one apply. ~/AGENTS.md points every agent at it with one line, so Claude Code, Codex, Copilot, and whatever comes next all read the same inventory.
The rendered file is one line per tool: name, verified version, a summary of what it is, an optional usage hint, and a docs link the agent can fetch when it needs option-level detail:
- `rg` 14.1.0 ripgrep, recursive regex search over directory trees,
gitignore-aware and fast; prefer over `grep -r` (docs:
https://github.com/BurntSushi/ripgrep)
- `ast-grep` 0.44.1 structural code search and rewrite over ASTs (tree-sitter,
20+ languages); prefer over regex for code-aware search and refactors (docs:
https://ast-grep.github.io/)
- `shellcheck` 0.9.0 static analysis for shell scripts, catches quoting,
portability, and logic bugs; run it on any shell script you write or edit
(docs: https://www.shellcheck.net/wiki/)Why a curated list instead of dumping PATH? Because a dev box has a thousand-plus binaries and the value of the file is signal, not enumeration. Agents already assume coreutils exist; the entries that earn their context cost are the ones an agent would not otherwise try (dotpro, glab, kubeseal), the ones with a naming trap (fd vs fdfind, difft for difftastic), and the guidance attached ("prefer over grep -r", phrasing that works better than "do not use grep"). A raw PATH scan can generate none of that, and it churns the file on every stray binary. The curated scan is deterministic: same machine, same file.
Extending it is one host_vars entry:
agents_tooling_path: ~/.agents/TOOLING.md # default
agents_tooling:
- name: doctl
version: doctl version # default: `<name> --version`
summary: "DigitalOcean CLI"
hint: "manages the droplets behind the staging fleet"
docs: https://docs.digitalocean.com/reference/doctl/What to install
The list only helps if the tools are there. When Bozhidar Batsov asked Claude Code which tools it wished it had, its top pick was ast-grep, structural search where regex is fragile, followed by difftastic and shellcheck. Trail of Bits installs the same tier by default, and benchmarks credit the rg/fd/ast-grep stack with 2 to 3x faster agent dev loops, simply because agents run these commands constantly.
Not everything in that tier deserves the same home, though. The line worth drawing is who wants the tool. ripgrep, fd, tree, shellcheck, jq, and yq are standard workstation kit that humans reach for with or without agents, so they live in the ubuntu / macos baselines. ast-grep, difftastic, sd, and shfmt are on the machine because agents use them; almost no human types ast-grep at a prompt. Those are the agent-motivated packages, and they ship with the agents role itself (agents_packages): Homebrew formulae on macOS, apt where it can on Ubuntu, GitHub releases for the two apt doesn't carry. Install and advertise then travel together, and a machine that runs no agents skips the whole tier by skipping the role. The fd naming trap is handled where it belongs, at provisioning time, with a /usr/local/bin/fd link to Ubuntu's fdfind, so the probe an agent runs matches the name it expects.
Three layers, one apply
The pieces reinforce each other, and each lives in the role that owns it:
- Install the
ubuntu/macosbaselines carry the human kit, and theagentsrole adds the agent-motivated packages on top, all versioned and upgraded like everything else dotpro manages - Advertise the
agentsrole scans and regenerates~/.agents/TOOLING.mdon every apply, and~/AGENTS.mdpoints all agents at it - Pre-approve the
clauderole allowlists the read-only tools (rg,fd,ast-grep,tree,shellcheck,difft,jq,yq), so knowing about a tool and being allowed to run it arrive together
None of the layers is novel on its own; people hand-write the middle one every day. The point of putting them in a provisioning run is the property the hand-written version can never have: the inventory is verified. The file says ast-grep 0.44.1 because the machine answered, not because someone remembered to update a markdown file. Agents are the one audience that will never forgive drift, because they believe everything you tell them.