The first fifteen minutes of an agent loop usually die on phantom toolchains, not on weak models. I refuse the session until the runtime prints a hashed manifest of the tools it can actually execute. That one gate is the only developer-experience fix that kept surviving contact with a disposable box. Why do we keep prompting as if the kitchen already lit the stove and stocked the pans?
I used to treat those opening minutes like a brainstorm, which felt collaborative and strangely professional. The model would sketch a test runner, invent a lint command, and reach for a package manager that only existed in training data. Meanwhile the shell on the other side of the session had never said its own name out loud. Is that a model quality problem, or is it a missing handshake between two strangers who share a transcript?
The friction looks intellectual because the text is fluent and the file names sound responsible. It is closer to walking into a rental kitchen and preheating an oven that was never wired for the building. You can write a beautiful mise en place and still serve nothing edible to the table. An agent that proposes npm test on a box without Node is not being creative. It is filling silence with a toolchain it cannot see.
I started watching the quarter hour as a tiny tragedy in four beats, and the pattern got boring fast. Beat one is confidence, because the prompt mentioned a language and the model assumed a whole studio around it. Beat two is theater, because it writes files that would work on a laptop it has never booted. Beat three is confusion, because the command fails and the loop retries with flags instead of asking what exists. Beat four is me, still chatting, still paying with attention. Does any of those beats need a smarter completion, or do they just need command -v?
The analogy I cannot shake is a lighting plot drawn for a stage that nobody surveyed. You can cue follow spots until midnight and still perform a reading in the dark. Free coding sessions fail the survey, then compensate with extra dialogue that feels like progress. I wanted the survey on paper before anyone was allowed to speak in patches. The loop can be fast. The shell is still a stranger until it prints.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I mention MonkeyCode only because its free model access and free server option let me pin that survey to a machine I can throw away without building a private fleet. The product is not the thesis. The thesis is that an agent loop should not start writing until the runtime can name the binaries it will actually run.
The proposed canary below is a workflow, not a benchmark, and I run it before the first edit lands. Drop the script on the free server, execute it once, and keep the model quiet until the JSON file exists. If the script exits nonzero, I stop the session and fix the box by hand like an adult. Would you accept a pull request from a teammate who had never run uname on the host?
#!/usr/bin/env bash
# proposed toolchain canary — run on the session server, not inside the prompt
set -euo pipefail
out="${1:-toolchain.manifest.json}"
tmp="$(mktemp)"
dump_cmd() {
local name="$1"
if command -v "$name" >/dev/null 2>&1; then
printf ' {"name": "%s", "path": "%s", "version": "%s"}' \
"$name" \
"$(command -v "$name")" \
"$("$name" --version 2>/dev/null | head -n 1 | tr -d '"')"
else
printf ' {"name": "%s", "path": null, "version": null}' "$name"
fi
}
{
echo '{'
echo " \"host\": \"$(uname -a | tr -d '\"')\","
echo " \"pwd\": \"$(pwd)\","
echo " \"user\": \"$(id -un)\","
echo " \"shell\": \"${SHELL:-unknown}\","
echo ' "tools": ['
first=1
for cmd in git node npm python3 pip3 go rustc cargo java mvn docker make gcc clang pytest; do
if [ "$first" -eq 1 ]; then first=0; else echo ','; fi
dump_cmd "$cmd"
echo
done
echo ' ]'
echo '}'
} > "$tmp"
python3 - <<'PY' "$tmp" "$out"
import hashlib, json, sys
src, dest = sys.argv[1], sys.argv[2]
with open(src, encoding="utf-8") as f:
data = json.load(f)
payload = json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
data["sha256"] = hashlib.sha256(payload).hexdigest()
with open(dest, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
print(dest)
print(data["sha256"])
PY
After the canary writes toolchain.manifest.json, I copy the file and the printed hash into the first real message and I do not paste a tree. The free model access then gets one job: read the manifest, name the commands it is allowed to call, and stay silent about everything else. If python3 is present and pytest is null, the test plan has to use the standard library until I install something. If node is null, there is no honest JavaScript patch to discuss yet. Are we debugging a product idea, or are we arguing with a ghost compiler?
Session contract (paste after the manifest, not before):
1. Treat toolchain.manifest.json as the only source of truth for executables.
2. If a tool has path null, do not emit commands that invoke it.
3. Propose at most one verification command drawn from tools with a real path.
4. Do not edit application files until that command exits 0 on this server.
5. If you need a missing tool, stop and ask me to install it; do not invent a fallback stack.
The gate I actually care about is boring, which is why it works when the chat gets exciting. A tiny checker sits next to the repo and fails closed unless the hash from the canary is exported into the environment. I run it on the free server as the first command after login, and I treat a nonzero exit as a locked door. The agent can still talk. It cannot pretend the room is furnished.
# proposed gate: fail closed unless this process saw the canary hash
import hashlib, json, os, sys
from pathlib import Path
manifest = Path("toolchain.manifest.json")
if not manifest.is_file():
sys.stderr.write("no toolchain manifest; refuse the session\n")
sys.exit(2)
data = json.loads(manifest.read_text(encoding="utf-8"))
reported = data.pop("sha256", "")
payload = json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
actual = hashlib.sha256(payload).hexdigest()
env_hash = os.environ.get("TOOLCHAIN_HASH", "")
if reported != actual or env_hash != actual:
sys.stderr.write("manifest hash mismatch; refuse patches\n")
sys.exit(3)
present = [t["name"] for t in data.get("tools", []) if t.get("path")]
print("toolchain ok:", ",".join(present))
The commands around that gate are intentionally unromantic, because romance is how I used to lose the quarter hour. I log into the free server, run the canary, export the hash, and only then open the model session. If I skip a step, the gate yells, and that yell is cheaper than reviewing a fictional Dockerfile. The model is allowed to be wrong about architecture. It is not allowed to be wrong about whether go exists.
chmod +x toolchain_canary.sh
./toolchain_canary.sh
export TOOLCHAIN_HASH="$(python3 -c 'import json; print(json.load(open("toolchain.manifest.json"))["sha256"])')"
python3 toolchain_gate.py
When the first command after the gate still fails, I do not widen the prompt. I reopen the manifest and ask a narrower question: did the binary exist, or did a library import fail later? That split matters, because PATH lies in a different way than import foo lies. The canary catches missing compilers and missing package managers. It will not catch a Python package that only breaks inside a test. If the loop cannot tell those failures apart, it will thrash for the rest of the fifteen minutes while sounding busy.
I keep a small decision map in my head rather than a dashboard, because dashboards turn into another prompt. If the canary cannot run, I fix the server and I do not chat. If a required language binary is null, I install it or I change the task. If the binary exists and the tests still fail, then the model may propose a patch. If the model names a tool that the manifest marked null, I throw the proposal away without reading the diff. Which of those branches looks like intelligence work, and which one looks like hospitality for a confused guest?
This approach has sharp edges, and I will not sand them off for a prettier story. The canary only reports what it finds on PATH, so a version manager hiding runtimes in a plugin can still lie. A free server is not your production image, so a green canary does not prove latency, memory, or security posture. I am not claiming quotas, model names, hardware, or duration for anyone else's account. If your work needs a sealed, identical replica of production, this handshake is a greeting, not a certificate.
People who should skip this ritual are easy to name once you drop the romance. Do not use it if you already boot a locked CI image whose toolchain is pinned and attested. Do not use it for design talk that never has to execute. Do not use it in shops that forbid third-party free servers, because a manifest from a machine you cannot run is just another hallucination with JSON makeup. And do not use it as an excuse to skip reading the diff after the gate opens. The gate only stops the phantom binaries. It will not stop a bad idea that compiles.
The one fix that mattered was not a cleverer first prompt and not a longer pep talk about being careful. It was refusing free model access the right to emit a patch until the free server had confessed its tools in a hashed file. After that, the opening quarter hour either produces a real command or it ends, which is still a kind of progress. If you already keep a disposable box around for coding agents, pin the next session there and withhold the first diff until the canary hash shows up.
Top comments (0)