Skip to content
Guard0

The Checklist Manifesto, for Agents

A twelve-item pre-flight for an agent repository: scan, fix, inventory, record. The Army's answer to too much airplane was not a better pilot. It was a list.

· 13 min read · Jayesh Bapu Ahire

The Checklist Manifesto, for Agents

On October 30, 1935, Boeing's Model 299 taxied out at Wright Field in Dayton with a crowd watching. Two months earlier it had made the delivery flight from Seattle in nine hours and three minutes. At the controls was the Air Corps' chief test pilot, Major Ployer P. Hill. The airplane climbed, stalled and crashed. Hill died of his injuries. Investigators found that the elevator and rudder gust lock, a device recently added to keep the control surfaces from banging about in wind on the ground, was still engaged. It had to be released from inside the cockpit, and nobody remembered.

Atul Gawande tells this story at the start of The Checklist Manifesto (2009), and the line he pulls from it comes from a newspaper he does not name: the Model 299 was "too much airplane for one man to fly." The Army's response was not to find a better pilot. Hill was the better pilot. The response was a written list, because the procedures were, in Phillip Meilinger's history of the crash, "too numerous and complicated to commit entirely to memory." Gawande's summary of what followed: "With the checklist in hand, the pilots went on to fly the Model 299 a total of 1.8 million miles without one accident." The Army ordered almost thirteen thousand of them and called it the B-17.

The book exists because the same trick worked in surgery, where it was then tested in a way nothing in this essay has been. Peter Pronovost's five-step list for inserting a central line went out across 103 reporting ICUs in Michigan's Keystone project, and the median infection rate per 1,000 catheter-days fell from 2.7 to 0 within three months and held, up to 66% lower, across 18 months. Three years later the WHO trial Gawande helped run, in eight hospitals from Toronto to Ifakara, reported deaths falling from 1.5% to 0.8% and inpatient complications from 11.0% to 7.0%. Those are peer-reviewed results, replicated across hospitals and counted in deaths that did not happen. Nothing below has been tested that way, and what I am borrowing from Gawande is a method and not a result: the list that follows was written by me and checked against a single repository I also wrote. Its only claim is that it is shorter than the thing it covers, and that a list you run every time beats a reviewer who is sure they remember.

None of those lists contained anything a surgeon did not already know. That is the point: when a system exceeds one expert's attention, the fix is not a better expert but a short, ordered list of the known failures, checked every time.

An agent repository is too much system for one reviewer. This is the list.

Black-and-white photograph of the Boeing Model 299 prototype lying broken on the grass at Wright Field, its tail and rear fuselage intact while the front burns under a column of black smoke.
Wright Field, Dayton, 30 October 1935: the Boeing Model 299 after a takeoff with the gust lock still engaged. The Army's answer was not a better pilot. It was a list. US Army Air Corps photograph.

Before you open the repo

The first item happens before the agent reads a file, because on September 1 Manifold Security showed that opening a folder is already execution. "Open a folder with Claude Code and it runs git status before you type anything. Before the workspace-trust prompt. On some agents, before you have even authenticated." Git reads core.fsmonitor from the repository's own .git/config. "The repository names a command, git runs it, on the host, with the user's privileges, before any approval prompt." Claude Code fixed the core.fsmonitor startup path in 2.1.196 and Codex CLI in 0.131.0, but Manifold confirmed a second Claude Code path, through claude ultrareview, still live on 2.1.252, with Qwen Code unpatched at retest and Hermes fixed only on September 2.

So the pre-flight starts outside the agent, before the repo is opened:

git config --get core.fsmonitor          # must print nothing
git -c core.fsmonitor=false status       # the safe form of the call the agent makes
git config --global core.fsmonitor false # and off by default for every repo

Then the version floors, because two LangGraph deserialization advisories landed this year and each fix is one line in requirements.txt. CVE-2026-28277 is unsafe msgpack deserialization in checkpoint loading: an attacker who can write checkpoint bytes at rest "can potentially supply a crafted payload that triggers unsafe object reconstruction when the checkpoint is loaded." Fixed in langgraph 1.0.10, scored 6.8, and the advisory is candid that it is "a post-exploitation / defense-in-depth issue" with "no evidence of exploitation in the wild." CVE-2026-27794 is the pickle fallback: before langgraph-checkpoint 4.0.0, BaseCache defaulted to JsonPlusSerializer(pickle_fallback=True), so a failed msgpack decode fell through to pickle.loads. Caching is off by default and the fix disables the fallback. And because the repo below uses a SQLite checkpointer, CVE-2025-67644, a SQL injection in that checkpointer's metadata filter keys, sets langgraph-checkpoint-sqlite at 3.0.1. Set LANGGRAPH_STRICT_MSGPACK=1 while you are there.

Two neighbours belong on the card. Langflow "enables unauthenticated auto-login by default," in VulnCheck's Caitlin Condon's words, which is why CVE-2026-5027, a path traversal in POST /api/v2/files, needed no credentials: "a single unauthenticated request is sufficient to obtain a valid session token before proceeding with exploitation." Roughly 7,000 instances were exposed, per Censys data cited by VulnCheck and inflated by up to twelve months of historical scans, and exploitation was confirmed in the second week of June, two months after the fix shipped in 1.9.0. Set LANGFLOW_AUTO_LOGIN=false. CrewAI's four CVEs in CERT/CC's VU#221883 include a CodeInterpreter tool that "falls back to SandboxPython when it cannot reach Docker," a sandbox that by the vendor's own admission left ctypes off its blocked-module list. The tool is gone from current releases and allow_code_execution is deprecated; confirm your version is one of them.

The scan, with real output

The repository is a two-file LangGraph customer-support agent: a StateGraph, a SqliteSaver checkpointer, gpt-4o, and three @tool functions named lookup_order, delete_customer and post_to_slack. It is the shape of the first agent most teams ship. The scan is one command.

$ npx @guard0/g0 scan .

Framework: langchain
Files scanned: 2
Agents: 1  Tools: 3  Prompts: 0
Duration: 1.3s
Analyzability: 100% (0 opaque files)
DB Accesses: 4 (2 unparameterized)
Call Graph Edges: 1

CRITICAL  Database tool without input validation [AA-TS-002] [AGENT REACHABLE]
Tool "lookup_order" in agent.py accesses a database without apparent input validation.
agent.py:13
Fix: Use parameterized queries and validate all input before database operations.
Standards: OWASP:ASI02 | AIUC-1:B003,D002 | ISO42001:A.6.2 | NIST:MAP-2.3

...

CRIT  3   HIGH  8   MED   6   LOW   6   INFO  0
Total: 23 findings

Domain Scores
Tool Safety            ██████████████████░░░░░░░░░░░░ 59 (7 findings)
...

Overall Score
F  ██████████████████████░░░░░░░░░░░░░░░░░░ 55
⚠ Grade capped: 3 exploitable critical findings
Security: 90/100  |  Hardening: 99/100

+ 22 utility-code findings suppressed (use --show-all)
+ 2 low-confidence findings hidden (use --min-confidence low)

A few things to read off that. The framework label says langchain even for a pure StateGraph app, because one parser covers both, and per the framework docs it is looking for agent definitions, tool bindings, prompt templates, memory and "LangGraph state machines and node connections." The rules behind the score number 1,128 across twelve domains, the largest being Tool Safety at 154. Security is 90 and Hardening 99, and the grade is still F, because the grade is capped when exploitable criticals are present: "a project with serious issues can never read as healthy." Twenty-three findings are visible; with --show-all --min-confidence low the JSON carries 47.

The fix, in order

Three criticals capped the grade. Fix them in this order, re-scanning after each.

First, the raw SQL. AA-TS-002 fired on lookup_order; with --show-all, AA-TS-075 fired on the same line for string concatenation. The diff is the one every database tutorial teaches:

-    cur.execute(f"SELECT * FROM orders WHERE id = '{order_id}'")
+    cur.execute("SELECT * FROM orders WHERE id = ?", (order_id,))

Do the same in delete_customer, the tool the scanner under-describes.

Second, the stop. AA-HO-005, "No emergency stop mechanism," is critical because a loop with no stop runs until the tokens or the database run out. The smallest fix a reviewer can verify is a guard node that checks for a stop file before any tool executes:

+STOP = Path.home() / ".support-agent.stop"
+
+def guard(state):
+    if STOP.exists():
+        raise RuntimeError("emergency stop engaged")
+    return state
+
 graph.add_node("guard", guard)
 graph.add_edge(START, "guard")

A stop the agent could delete is not a stop; AA-RA-060 "Kill switch bypassable by agent" exists for that reason, so the file lives outside the agent's write path.

Third, the side effect. post_to_slack posts to a channel on the agent's decision. Under --show-all it fired AA-HO-075 "No human review for external API calls" and AA-RA-041 "Agent has unrestricted network access from sandbox." LangGraph answers this natively: interrupt the graph before that node and let a human resume it.

-app = graph.compile(checkpointer=SqliteSaver.from_conn_string("support.db"))
+app = graph.compile(
+    checkpointer=SqliteSaver.from_conn_string("support.db"),
+    interrupt_before=["post_to_slack"],
+)

Then re-scan. What you are checking is not the overall score, which stays low while findings you have not reached are still open, but those three identifiers and the capped-grade line. In a pipeline that check is g0 gate --no-critical, which exits 1 and prints its reason.

Now the two things the scanner does not do, because a checklist that hides its gaps is the Model 299 with a missing line. The checkpointer surfaced only as AA-MP-071, "Persistent memory stored without encryption," medium severity, confidence low, hidden by default. No AA-* rule covers LangGraph checkpoint deserialization: g0 rules list --search msgpack and --search langgraph both return "No rules found matching the given filters," and AA-SC-023 "Known vulnerable dependency" embeds no LangGraph or Langflow CVE identifiers in the 2.1.0 package. On this repo the unpinned langgraph line raised AA-SC-001 "Unpinned Python dependencies" and a missing-lockfile finding, which is correct and insufficient. The version floors above are a dependency scanner's job; put them on the card by hand.

The second gap is delegation. AA-IC-117 "Missing identity chain in delegation" stayed quiet because this graph has one agent, and "Delegation Without Trust" tested what happens when you add the second: across LangGraph, CrewAI, AutoGen and the MCP authorization model, "three provide no built-in confinement and one only partial." Nothing in the framework will build you that chain.

The bill of materials and the record

A scan is a point in time. Two more commands turn it into something you can diff and hand to an auditor.

$ g0 inventory . --gen-key k
Keypair written: k.key (private, keep safe) / k.pub (public)
$ g0 inventory . --cyclonedx --sign-key k.key -o bom.json

The output is a CycloneDX 1.6 document: a machine-learning-model component for gpt-4o, library components for the frameworks and each tool, an application component for the agent, and an ed25519 signature block with a bomHash, the public key and a timestamp. The BOM is "content-addressed so it diffs across releases," and g0 inventory . --diff <baseline> does the diff. Run it weekly; the week the hash moves and nobody shipped anything is the week to go find out why.

g0 flows . is the one command I would keep if I could keep only one, because it answers what the scan does not: where does untrusted input end up?

$ g0 flows .
Nodes: 5  Edges: 4  Paths: 3
[55] User Input -> graph -> lookup_order
[55] User Input -> graph -> delete_customer
[15] User Input -> graph -> post_to_slack
HIGH  Untrusted input reaches database write
      User input flows to "delete_customer" which writes to database without parameterization
Total paths: 3  Toxic flows: 2  Max risk: 55

Two toxic flows, and the second is a customer's chat message reaching a tool that deletes customers.

The last step on the card is runtime, and it is the step this essay cannot walk you through. g0 protect is on main, listed under "[Unreleased]" in the CHANGELOG and absent from the 2.1.0 package on npm, so the workflow above ends at inventory and flows. I am not going to preview it, because a tour of a feature nobody can run is a sales call, not a checklist, and the useful thing to know is the proxy's ceiling: MCP proxies, ours included, cannot see Claude Code's built-in tools, because Bash commands and file writes never traverse MCP. Closing that gap is what the hook surface is for, and it is unreleased too.

The card

Twelve lines, every time, grouped by who supplies the check. Eight name no product and survive whatever tooling you choose. Config. Inspect .git/config for core.fsmonitor, core.hooksPath and filters before any agent opens it. Floors. langgraph at 1.0.10, langgraph-checkpoint at 4.0.0, langgraph-checkpoint-sqlite at 3.0.1 (langgraph-checkpoint-postgres at 3.0.3, @langchain/langgraph-checkpoint-redis at 1.0.2), langchain-core at 1.2.22, langflow at 1.9.0, pinned and locked. Login. No auto-login on any framework UI. Sandbox. No in-process code interpreter fallback; agent code runs off the host. Parameterize. Every database tool takes parameters, never strings. Stop. A kill switch outside the agent's write path. Gate. A human interrupt before every tool that sends, pays, deploys or deletes. Isolate. One checkpointer thread per user, strict msgpack, no pickle fallback.

The remaining four name a command from the tool my team builds, which means you should read the check and not the command. Scan. npx @guard0/g0 scan ., criticals before score, or any analyzer that reads your tool definitions, or an hour spent reading them yourself. Flows. g0 flows . with zero toxic paths, or a written reason for each that remains; by hand, that is tracing from the input node to every tool that writes. Inventory. A signed CycloneDX BOM, diffed against last week's, from any generator that emits a digest. Record. A build that fails on findings new since a baseline, g0 gate . --baseline in our case, and runtime enforcement the day you have something that can do it.

Four grouped stages of a twelve-item agent pre-flight: scan, fix, inventory, and record, with record marked as the one that must exist beforehand.
Too much airplane for one person to remember.

What the card costs

A checklist matters most on the days you are sure you do not need it, so I will be plain about the three places this one costs something.

The first is the fix step, which cannot be delegated to the agent that wrote the code. PatchBench, submitted September 3, evaluated eleven agents at C/C++ vulnerability patching and found that "25% of the agent patches exhibit substantial similarity to historical developer patches," which the authors read as memorization, and that agents "frequently exploit benchmark structures to pass patch validation by patching on the crash stack trace to suppress the crash." Proof-of-concept-only validation inflated solve rates "by 1.83× on average." The scope is C/C++, not Python agent code, and I would not stretch it further. The mechanism travels, though: an agent asked to make a scanner finding go away will make the finding go away, so the three diffs above are the ten minutes of human reading the rest of the card depends on.

The second is what the scanner missed here. Two of the three tools in this repo were under-described by the tool that grades tools: the delete rules stayed silent on delete_customer and the side-effect rules stayed silent on post_to_slack, and the previous essay in this series carries the rule identifiers and the reproduction. Both are open bugs against us. Until they close, items six through eight on the card are yours to check by eye.

The third is time, where the news is better than people fear. Two files scanned in 1.3 seconds. The noise costs more than the runtime, and what makes CI bearable is --ruleset recommended, about 200 high-signal rules, then g0 gate . --write-baseline base.json once and g0 gate . --baseline base.json afterward, so the build fails only on findings new since the baseline. The 2.1.0 changelog reports clean-agent critical and high false positives dropped from 17 to 4 on our validation corpus. This repo produced two more of its own. AA-TS-024 "Tool with write to shared state" and AA-CF-042 "Agent modifies shared infrastructure," both high, fired on this line:

slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])

That is a read of an environment variable at import. The fix text says "Avoid modifying global state from tools," which does not apply, and the response is the suppression comment the docs provide, # g0-ignore: reads env at import, with the reason written down for the next reviewer. A false positive you annotate is a line on the card. A false positive you silence with --exclude-rules is a step you stopped checking.

Four bars showing surgical death rates falling from 1.5 to 0.8 percent and complication rates from 11.0 to 7.0 percent after a nineteen-item checklist.
Twelve lines, every time.

Too much airplane

The Model 299 did not become safe because the Army found a pilot who could hold the whole airplane in his head. Nobody could; that was the newspaper's point. It became safe because the people who flew it wrote down the handful of things that had to be true before it left the ground, and ran the list every flight, including the flights where it felt unnecessary, which was all of them.

An agent repo has the same property. The reviewer who can hold the checkpointer's serializer, the tool's SQL, the graph's stop condition and the Slack token's blast radius in one head does not exist, and staffing as though that reviewer exists is what the Air Corps stopped doing after Wright Field. The list above is short, ordered and boring. On this repo it moves the grade from F to a build that passes its own gate, and the loop there is worth naming: the repo is mine, the F is my scanner's opinion of it, and the signature is my command. That proves nothing about the list. What survives the loop is the three diffs, the version floors and the two gaps, and those are the same in a repository nobody here wrote.

Gust lock off. Then fly.


Every command in this essay is the free, open-source g0 scanner at version 2.1.0 and runs today, except the runtime step, which is on main, unreleased, and deliberately not demonstrated here. The two false negatives are filed against ourselves, and the eight product-free lines on the card are the ones I would keep if you never install any of it.

References

  1. Gawande, "The Checklist," The New Yorker, December 10, 2007
  2. Meilinger, "When the Fortress Went Down," Air Force Magazine, October 2004
  3. Pronovost et al., NEJM 2006: an intervention to decrease catheter-related bloodstream infections in the ICU
  4. Haynes et al., NEJM 2009: a surgical safety checklist to reduce morbidity and mortality
  5. Manifold Security: GitSpawn
  6. GHSA-g48c-2wqr-h844: LangGraph checkpoint loading has unsafe msgpack deserialization (CVE-2026-28277)
  7. SentinelOne on CVE-2026-27794
  8. The Hacker News on Langflow CVE-2026-5027
  9. CERT/CC VU#221883: CrewAI
  10. Delegation Without Trust (arXiv 2609.00267)
  11. PatchBench (arXiv 2609.04075)
  12. g0 on GitHub

The Signal · AI Agents · Security

Jayesh Bapu Ahire
Founder, Guard0