<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Engineering Guides · The Durability Curve</title><description>Every piece in Engineering Guides, in full.</description><link>https://durabilitycurve.com/</link><item><title>What Is Prompt Injection? How to Stop a Tricked AI Agent Leaking Your Data</title><link>https://durabilitycurve.com/blog/what-is-prompt-injection/</link><guid isPermaLink="true">https://durabilitycurve.com/blog/what-is-prompt-injection/</guid><description>One GitHub issue got an agent to post private-repo data in public. Why no filter fully stops it, which guards held in our runs, and how to test yours.</description><pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p class=&quot;lede-dek&quot;&gt;&lt;em&gt;One GitHub issue got an agent to post private-repo data in public. Why no filter fully stops it, which guards held in our runs, and how to test yours.&lt;/em&gt;&lt;/p&gt;
&lt;p data-lead-img=&quot;&quot;&gt;&lt;img alt=&quot;A public GitHub issue, a private repository and a public pull request on one desk: the agent read the first, copied from the second and wrote the third.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;819&quot; src=&quot;https://durabilitycurve.com/_astro/prompt-injection-hero-anim.CEoWGKYI_1w6qgp.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt injection is when text inside something your AI agent reads, such as an email, a web page or a GitHub issue, gets followed as if you had typed it.&lt;/strong&gt; OpenAI calls it “instructions placed in external content in an attempt to make the model do something the user did not ask for.”&lt;/p&gt;
&lt;p&gt;In May 2025 researchers at &lt;a href=&quot;https://invariantlabs.ai/blog/mcp-github-vulnerability&quot;&gt;Invariant Labs&lt;/a&gt; set up demo repositories and put an instruction in a public GitHub issue. When the repository’s owner asked Claude to look at the open issues, the agent read data from the owner’s private repositories and published some of it in a public pull request: the names of private projects, a plan to relocate to South America and the owner’s salary.&lt;/p&gt;
&lt;p data-cover-after=&quot;&quot;&gt;The tools were legitimate and the request was ordinary. Nothing in GitHub’s server code was at fault, Invariant says; the attack works “even with fully trusted tools”. The instruction came from a stranger.&lt;/p&gt;
&lt;p&gt;On 27 October we tested three coding agents ourselves, one run per setup. None followed the plain planted instructions we saw them open. Their guards were another matter: Claude Code’s fetch tool sits outside its shell sandbox, in bypassPermissions mode its sandbox let curl reach our website until one more setting was added, and Cursor’s sandbox let commands reach a listener on the same machine, which its docs say is blocked.&lt;/p&gt;
&lt;p&gt;This guide explains why that happens, what each guard controls, and how to test your own setup; the ten-minute check is under the lethal trifecta.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; a leak needs private data, text a stranger wrote and a way out, in one session. Take one away for any task that reads strangers’ text, then test the guard yourself: an agent that ignores a planted line tells you about the model and nothing about the guard.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;what-is-prompt-injection-direct-and-indirect&quot;&gt;What is prompt injection? Direct and indirect&lt;/h2&gt;
&lt;p&gt;To the owner, the issue was a stranger’s note to read later. To the agent it was text in the same stream as the owner’s request, and it read like a request. Models follow instructions in content, which is what makes them useful, and as Simon Willison &lt;a href=&quot;https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/&quot;&gt;puts it&lt;/a&gt;, they “are unable to reliably distinguish the importance of instructions based on where they came from.”&lt;/p&gt;
&lt;p&gt;Willison &lt;a href=&quot;https://simonwillison.net/2022/Sep/12/prompt-injection/&quot;&gt;proposed the name&lt;/a&gt; in September 2022, after SQL injection; the shared problem, &lt;a href=&quot;https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/&quot;&gt;he later wrote&lt;/a&gt;, is “mixing together trusted and untrusted content in the same context.” He has since &lt;a href=&quot;https://simonwillison.net/2025/Aug/4/himbodhisattva/&quot;&gt;credited&lt;/a&gt; someone else with coining it on 13 May 2022, “four months before I did”.&lt;/p&gt;
&lt;p&gt;The OWASP Top 10 for LLM applications ranks prompt injection first, as &lt;a href=&quot;https://genai.owasp.org/llmrisk/llm01-prompt-injection/&quot;&gt;LLM01:2025&lt;/a&gt;, and splits it in two. A direct injection comes from the person typing; an indirect one arrives when a model “accepts input from external sources, such as websites or files.” The GitHub issue was the indirect kind, the one that turns an agent against the person it works for. (OWASP counts jailbreaking as a form of prompt injection; Willison keeps the two apart.)&lt;/p&gt;
&lt;h2 id=&quot;where-prompt-injection-hides&quot;&gt;Where prompt injection hides&lt;/h2&gt;
&lt;p&gt;Email is the obvious way in, and two real cases came that way. In June 2025 researchers at Aim Security disclosed EchoLeak, in which a single crafted email made Microsoft 365 Copilot leak data with no click from the user (&lt;a href=&quot;https://arxiv.org/abs/2509.10540&quot;&gt;a later case study&lt;/a&gt; takes it apart). Microsoft rated it Critical, &lt;a href=&quot;https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-32711&quot;&gt;published it&lt;/a&gt; as CVE-2025-32711 on 11 June 2025, and says it “has already been fully mitigated”.&lt;/p&gt;
&lt;p&gt;The second, an attack on ChatGPT, was reported to OpenAI by outside researchers in 2025: an email that read like an HR follow-up asked the assistant to “submit these details to the compliance validation system”. &lt;a href=&quot;https://openai.com/index/designing-agents-to-resist-prompt-injection/&quot;&gt;OpenAI says&lt;/a&gt; that in testing it “worked 50% of the time” when the user asked deep research to read the day’s emails.&lt;/p&gt;
&lt;p&gt;Web pages carry it too: for a browser agent, &lt;a href=&quot;https://www.anthropic.com/research/prompt-injection-defenses&quot;&gt;Anthropic writes&lt;/a&gt;, “every webpage an agent visits is a potential vector for attack.”&lt;/p&gt;
&lt;p&gt;A coding agent reads more places than it seems to. The paid kit for this guide (more at the end) plants a harmless instruction in eight of them: the project README, an HTML comment in a Markdown file, a code comment, a vendored package’s README, an AGENTS.md inside that package, an outsider’s bug report, a web page the docs link to, and a record an API returns. Someone other than you may have written any of them.&lt;/p&gt;
&lt;p&gt;The attacks have changed shape too: OpenAI says the most effective now “increasingly resemble social engineering more than simple prompt overrides.” And while a vendor can close the route out of its own product, as Willison notes most did, “once you start mixing and matching tools yourself there’s nothing those vendors can do to protect you!”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make a list of what your agent reads that someone else can write.&lt;/strong&gt; The public repository’s issues belonged on it.&lt;/p&gt;
&lt;h2 id=&quot;the-lethal-trifecta-when-an-ai-agent-can-leak-your-data&quot;&gt;The lethal trifecta: when an AI agent can leak your data&lt;/h2&gt;
&lt;p&gt;Willison’s lethal trifecta is the most useful single test in this guide. Data theft needs three things together: “Access to your private data”, “Exposure to untrusted content”, and “The ability to externally communicate in a way that could be used to steal your data.” In his words: “If your agent combines these three features, an attacker can easily trick it into accessing your private data and sending it to that attacker.”&lt;/p&gt;
&lt;p&gt;Read the third leg as a way out: anything that sends data, or changes something other people can see. The GitHub pull request did both.&lt;/p&gt;
&lt;p&gt;The GitHub case had all three in one connector: it could read the private repositories, the public issue was untrusted content, and a pull request on a public repository was the way out.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The lethal trifecta as a triangle on the GitHub case: private data (the private repositories) at the top, untrusted content (the public issue) at the lower left and a way out (a pull request on a public repository) at the lower right, joined by lit edges, with GitHub&amp;#39;s MCP server named in the middle as the one connector that held all three.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;1112&quot; src=&quot;https://durabilitycurve.com/_astro/fig-trifecta-anim.pbHJdVYF_2ncLij.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;A way out is easier to have than it looks: any tool that calls an API, loads an image or shows you a link to click “can be used to pass stolen information back to an attacker,” Willison writes.&lt;/p&gt;
&lt;p&gt;Meta’s security team turned this into the &lt;a href=&quot;https://ai.meta.com/blog/practical-ai-agent-security/&quot;&gt;Agents Rule of Two&lt;/a&gt;: in any one session, an agent should have no more than two of untrusted input, private data, and the power to “change state or communicate externally”; a task that needs all three needs supervising. Willison &lt;a href=&quot;https://simonwillison.net/2025/Nov/2/new-prompt-injection-papers/&quot;&gt;adds&lt;/a&gt; that untrusted input plus the power to change things is risky even with nothing private in reach.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do this first (10 minutes):&lt;/strong&gt; list the AI tools and agents you use. For each, ask: can it read your private things? Does it read text strangers can write (mail, web pages, issues, dependencies)? Does it have a way out (send, post, open a pull request, fetch a URL)?&lt;/p&gt;
&lt;p&gt;For any tool with all three, take one away for the tasks that read strangers’ text: narrow its access, or move those tasks to a session with nothing private in reach. A coding agent usually scores three yeses, so close its way out and make its writes ask first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;aside class=&quot;paywall&quot; data-paywall=&quot;&quot;&gt;
  &lt;p class=&quot;paywall-label&quot;&gt;Paid subscribers&lt;/p&gt;
  &lt;p class=&quot;paywall-body&quot;&gt;The rest of this piece is for paid subscribers, on any tier.&lt;/p&gt;
  &lt;p class=&quot;paywall-act&quot;&gt;&lt;a href=&quot;https://harryfloyd.substack.com/p/what-is-prompt-injection&quot;&gt;Read the rest on Substack&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;</content:encoded></item><item><title>Claude Code Skills: How They Work, How to Write One, and How to Check Claude Uses It</title><link>https://durabilitycurve.com/blog/claude-code-skills/</link><guid isPermaLink="true">https://durabilitycurve.com/blog/claude-code-skills/</guid><description>Claude picks a skill from its name and description. How to write the two lines it reads, and check it chose yours.</description><pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p class=&quot;lede-dek&quot;&gt;&lt;em&gt;Claude picks a skill from its name and description. How to write the two lines it reads, and check it chose yours.&lt;/em&gt;&lt;/p&gt;
&lt;p data-lead-img=&quot;&quot;&gt;&lt;img alt=&quot;41 skills loaded, drawn as a row of glass cards fading into the distance. The front card is helper, &amp;quot;Does stuff with files&amp;quot;. A request for a commit message travels down the row, and the readout says helper was chosen 0 of 12 times.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;819&quot; src=&quot;https://durabilitycurve.com/_astro/claude-code-skills-hero-anim.CG7TN22W_Z27bjI.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A Claude Code skill is a folder of instructions that Claude loads when a task calls for it.&lt;/strong&gt; It follows an open format that the Claude app, ChatGPT and Codex also read: the &lt;code&gt;SKILL.md&lt;/code&gt; file travels between them, though how you install it, when it runs on its own and which fields count all differ.&lt;/p&gt;
&lt;p&gt;I wrote one that writes commit messages in a house format and gave it the name and description &lt;a href=&quot;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices&quot;&gt;Anthropic’s guide to writing skills&lt;/a&gt; tells you to avoid: &lt;code&gt;helper&lt;/code&gt;, and “Does stuff with files”. Typed as &lt;code&gt;/helper&lt;/code&gt;, it worked.&lt;/p&gt;
&lt;p data-cover-after=&quot;&quot;&gt;Asked in plain words for a commit message, Claude passed it over 12 times out of 12 (four phrasings, three runs each, Claude Opus 5.5, one repository) and wrote decent messages of its own, with nothing in the reply to say a skill had been skipped.&lt;/p&gt;
&lt;p&gt;The instructions were fine. Claude never chose them.&lt;/p&gt;
&lt;p&gt;The point of a skill is that you stop having to remember it, so one that runs only when you type its name has done half its job. This guide takes &lt;code&gt;helper&lt;/code&gt; from that first draft to a version Claude chose on all 21 commit-message requests in my runs, and ends with the check that tells you which kind you have. (If a well-written skill of yours has stopped being chosen, &lt;a href=&quot;https://durabilitycurve.com/blog/skill-that-never-fired/&quot;&gt;The Skill That Never Fired&lt;/a&gt; covers why.)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; Claude chooses a skill mainly from two lines, its name and its description. Give it a name that says the job and a description that says what it does and when to use it, in the words you would type. Then check the choice: in Claude Code, a &lt;code&gt;Skill&lt;/code&gt; call shows that Claude picked the skill, and a reply in the right format doesn’t. Section 3 shows how to write a skill, and section 6 how to check it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;what-are-agent-skills&quot;&gt;What are agent skills?&lt;/h2&gt;
&lt;p&gt;Agent skills is the name the format goes by outside Claude Code. Anthropic created it, describing a skill as “a directory containing a SKILL.md file” whose instructions, scripts and resources “give agents additional capabilities,” and in December 2025 it “published Agent Skills as an open standard for cross-platform portability.”&lt;/p&gt;
&lt;p&gt;Claude Code’s skills “follow the Agent Skills open standard, which works across multiple AI tools,” and OpenAI’s skills for ChatGPT and Codex “build on the open agent skills standard.”&lt;/p&gt;
&lt;p&gt;Inside Claude Code, a skill is easy to confuse with four other things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CLAUDE.md&lt;/strong&gt; is read at the start of every session, so it suits facts the agent always needs, while a skill’s body “loads only when it’s used,” which suits a procedure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A custom slash command&lt;/strong&gt; is now the same thing: “Custom commands have been merged into skills.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A subagent&lt;/strong&gt; “runs in its own context window”, while an invoked skill’s content “enters the conversation as a single message and stays there across later turns.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt; is “an open-source standard for connecting AI applications to external systems”; a skill can tell the agent how to use what MCP connects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude Code’s docs give the test for when to write one: “Create a skill when you keep pasting the same instructions, checklist, or multi-step procedure into chat, or when a section of CLAUDE.md has grown into a procedure rather than a fact.” A house commit format is that kind of procedure.&lt;/p&gt;
&lt;h2 id=&quot;how-claude-chooses-a-skill&quot;&gt;How Claude chooses a skill&lt;/h2&gt;
&lt;p&gt;At the start of a session, in the words of &lt;a href=&quot;https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills&quot;&gt;Anthropic’s engineering post on skills&lt;/a&gt;, “the agent pre-loads the name and description of every installed skill into its system prompt.” The rest of the file waits: “If Claude thinks the skill is relevant to the current task, it will load the skill by reading its full SKILL.md into context.”&lt;/p&gt;
&lt;p&gt;So the choice rests mainly on two lines. My test sessions loaded 41 skills: &lt;code&gt;helper&lt;/code&gt; and 40 others that come with Claude Code or with my account, none of them about commits.&lt;/p&gt;
&lt;p&gt;Asked for a commit message, Claude had the name &lt;code&gt;helper&lt;/code&gt; and the words “Does stuff with files” to go on. The format, the ticket rule and the &lt;code&gt;Refs:&lt;/code&gt; line were all in the body, which the skill loads only once Claude picks it.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;What Claude reads before it chooses a skill (each skill&amp;#39;s name and description) and after (the chosen skill&amp;#39;s body). For helper, the two lines were &amp;quot;helper&amp;quot; and &amp;quot;Does stuff with files&amp;quot;.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;1006&quot; src=&quot;https://durabilitycurve.com/_astro/fig-before-after-anim.C8T7slE__d85CF.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude picks a skill from its name and description, and the rest loads only after it has picked.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are two exceptions. A skill set to run only by name (section 3) stays out of the listing, and once the listing outgrows its budget, by default 1% of the context window, Claude Code drops descriptions “starting with the skills you invoke least.”&lt;/p&gt;
&lt;h2 id=&quot;how-to-write-a-claude-code-skill&quot;&gt;How to write a Claude Code skill&lt;/h2&gt;
&lt;p&gt;A skill is a folder named after the skill, with a &lt;code&gt;SKILL.md&lt;/code&gt; inside. Here is the version I recommend: &lt;code&gt;helper&lt;/code&gt;’s instructions with two lines added, under a name that says the job and a description adapted from Anthropic’s example.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;font-weight:bold&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;name: writing-commit-messages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;description: Writes commit messages in the house format from the staged diff. Use when the user asks for a commit message or the message for changes about to be committed. Not for code review.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;font-weight:bold&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Write the commit message for the staged changes:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;1.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Read the staged diff (git diff --staged) and the branch name (git branch --show-current).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;2.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; First line: type(scope): summary, at most 60 characters. Types: feat, fix, docs, refactor, test, chore.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;3.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; A blank line, then one or two sentences on why the change was made.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;4.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; A last line: Refs: followed by the ticket ID from the branch name (PAY-142 from feat/PAY-142-retry-timeout). If the branch name has no ticket ID, leave the Refs: line out and never invent one.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Reply with the message only. Do not run git commit.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;Make the folder in your repository and save the file there as &lt;code&gt;.claude/skills/writing-commit-messages/SKILL.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;mkdir&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; .claude/skills/writing-commit-messages&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then stage a change, start Claude Code in the repository and ask for a commit message in your own words. The message should open with a &lt;code&gt;type(scope):&lt;/code&gt; line and, if your branch name carries a ticket, end with &lt;code&gt;Refs:&lt;/code&gt; and that ticket; section 6 shows how to confirm Claude chose the skill rather than writing the message itself.&lt;/p&gt;
&lt;p&gt;In my runs this version was chosen on all 21 commit-message requests, took none of 9 code-review requests, and on a branch with no ticket left the &lt;code&gt;Refs:&lt;/code&gt; line out all six times. I wrote its description after seeing the first two runs, so some of those requests are ones it was written around; test it on yours.&lt;/p&gt;
&lt;h3 id=&quot;what-changed-from-anthropics-example&quot;&gt;What changed from Anthropic’s example&lt;/h3&gt;
&lt;p&gt;Anthropic’s Git Commit Helper example, which the runs also tested, is the starting point. The name now says the job.&lt;/p&gt;
&lt;p&gt;The description is in the third person, as Anthropic’s page asks (“Always write in third person”), where the example opens with “Generate”; it drops “or reviewing staged changes”, a job this body doesn’t do, and adds “Not for code review”. The body says where to look and what to do when a branch has no ticket.&lt;/p&gt;
&lt;p&gt;On a branch with no ticket, the version with Anthropic’s description, in the third person, and the original body left a placeholder such as &lt;code&gt;Refs: &amp;#x3C;TICKET-ID&gt;&lt;/code&gt; in all six runs; the recommended version left the line out.&lt;/p&gt;
&lt;h3 id=&quot;the-description&quot;&gt;The description&lt;/h3&gt;
&lt;p&gt;The description is where you say when the skill applies, since a name has no room for it. &lt;a href=&quot;https://code.claude.com/docs/en/skills&quot;&gt;Claude Code’s skills docs&lt;/a&gt; define the field as “What the skill does and when to use it,” and the examples on Anthropic’s best-practice page follow one pattern: what the skill does, then “Use when” and the requests it is for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Put in the words you would type when you want the skill.&lt;/strong&gt; Keep it under 200 characters, the Claude app’s limit. Claude Code also takes a &lt;code&gt;when_to_use&lt;/code&gt; field for trigger phrases, which the Claude app rejects (section 4).&lt;/p&gt;
&lt;h3 id=&quot;the-name&quot;&gt;The name&lt;/h3&gt;
&lt;p&gt;The name is the folder’s name and the command you type: lowercase letters, numbers and hyphens, up to 64 characters, and &lt;a href=&quot;https://agentskills.io/specification&quot;&gt;the standard&lt;/a&gt; says it “Must match the parent directory name”.&lt;/p&gt;
&lt;p&gt;In my runs a name that says the job carried the choice even under the vague description, including on requests that never said “commit”. In &lt;a href=&quot;https://durabilitycurve.com/blog/skill-that-never-fired/&quot;&gt;the two-skill test in the earlier essay&lt;/a&gt;, “A good description rescued names that carried no signal,” and “A good name had already done the same for descriptions that carried none.” Routing broke down only when neither said what set the skill apart, and section 6 shows the same here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every tool reads the description, and it is the one portable field that can say when, so write it first.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-body&quot;&gt;The body&lt;/h3&gt;
&lt;p&gt;The body is the procedure. Keep it to steps Claude can follow, because “Once a skill loads, its content stays in context across turns, so every line is a recurring token cost.” Longer reference material can go in other files in the folder, which the body names and Claude opens “only as needed.”&lt;/p&gt;
&lt;h3 id=&quot;skills-with-side-effects&quot;&gt;Skills with side effects&lt;/h3&gt;
&lt;p&gt;One frontmatter field decides whether Claude may choose the skill at all. This skill is designed only to draft a message, so an unwanted invocation is low-risk; its “Do not run git commit” is an instruction, and tool permissions remain the separate safeguard.&lt;/p&gt;
&lt;p&gt;A skill that commits, deploys or sends something is different. Claude Code’s docs say to set &lt;code&gt;disable-model-invocation: true&lt;/code&gt; “for workflows with side effects or that you want to control timing, like /commit, /deploy, or /send-slack-message. You don’t want Claude deciding to deploy because your code looks ready.”&lt;/p&gt;
&lt;p&gt;With that set, the description leaves the listing and Claude can’t run the skill on its own; stopping the action itself is a job for the permissions covered in &lt;a href=&quot;https://durabilitycurve.com/blog/ai-agent-permissions/&quot;&gt;the second guide in this series&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-skills-live-in-claude-code-the-claude-app-chatgpt-and-codex&quot;&gt;Where skills live in Claude Code, the Claude app, ChatGPT and Codex&lt;/h2&gt;
&lt;p&gt;&lt;img alt=&quot;Where skills live: Claude Code reads ~/.claude/skills and .claude/skills; the Claude app takes a ZIP; ChatGPT lists them under Plugins for Business, Enterprise, Healthcare and Edu; Codex reads .agents/skills. Each tool can pick a skill itself, and each lets you ask for one by name.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;941&quot; src=&quot;https://durabilitycurve.com/_astro/fig-where-skills-live-anim.Cz1-95Cw_1X35n3.webp&quot; &gt;&lt;/p&gt;
&lt;h3 id=&quot;claude-code&quot;&gt;Claude Code&lt;/h3&gt;
&lt;p&gt;Claude Code reads skills from two places most people need. &lt;code&gt;~/.claude/skills/&amp;#x3C;skill-name&gt;/SKILL.md&lt;/code&gt; is yours and loads in all your projects on this machine. &lt;code&gt;.claude/skills/&amp;#x3C;skill-name&gt;/SKILL.md&lt;/code&gt; lives in a repository, and the docs’ advice is “Commit it so your team gets it too.” When two skills share a name, “Enterprise over personal, and personal over project,” so a personal &lt;code&gt;helper&lt;/code&gt; would replace the team’s.&lt;/p&gt;
&lt;h3 id=&quot;the-claude-app&quot;&gt;The Claude app&lt;/h3&gt;
&lt;p&gt;In the Claude app, skills sit under &lt;code&gt;Customize &gt; Skills&lt;/code&gt;; you upload the folder as a ZIP, and Claude “reviews available skills, loads relevant ones, and applies their instructions.” &lt;a href=&quot;https://support.claude.com/en/articles/12512180-use-skills-in-claude&quot;&gt;Anthropic’s help centre&lt;/a&gt; lists skills on the Free, Pro, Max, Team and Enterprise plans with code execution on, though the developer overview still lists only the paid plans.&lt;/p&gt;
&lt;p&gt;Uploads accept six frontmatter fields, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt;, &lt;code&gt;compatibility&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;allowed-tools&lt;/code&gt;; anything else, &lt;code&gt;disable-model-invocation&lt;/code&gt; and &lt;code&gt;when_to_use&lt;/code&gt; included, “fails with a hard error instead of ignoring the field.”&lt;/p&gt;
&lt;h3 id=&quot;chatgpt&quot;&gt;ChatGPT&lt;/h3&gt;
&lt;p&gt;In ChatGPT, according to &lt;a href=&quot;https://help.openai.com/en/articles/20001066-skills-in-chatgpt&quot;&gt;OpenAI’s help page&lt;/a&gt;, “Skills are available to eligible ChatGPT Business, Enterprise, Healthcare, and Edu users, subject to workspace settings and product availability.” They sit under Plugins, on a Skills tab, and you call one by typing @.&lt;/p&gt;
&lt;h3 id=&quot;codex&quot;&gt;Codex&lt;/h3&gt;
&lt;p&gt;Codex looks for skills in &lt;code&gt;.agents/skills&lt;/code&gt; in the repository and &lt;code&gt;~/.agents/skills&lt;/code&gt; in your home folder, and you mention one with $. To stop Codex choosing a skill on its own, set &lt;code&gt;allow_implicit_invocation: false&lt;/code&gt; under &lt;code&gt;policy:&lt;/code&gt; in the skill’s &lt;code&gt;agents/openai.yaml&lt;/code&gt;; then “explicit $skill invocation still works.”&lt;/p&gt;
&lt;h2 id=&quot;read-a-skill-before-you-install-it&quot;&gt;Read a skill before you install it&lt;/h2&gt;
&lt;p&gt;A skill someone else wrote goes into the same listing as yours, and whenever Claude picks it, its instructions join the conversation. Skills, Anthropic’s post notes, “can also include code for Claude to execute as tools at its discretion.”&lt;/p&gt;
&lt;p&gt;The post’s advice is to install skills “only from trusted sources” and, for anything less, to start “by reading the contents of the files bundled in the skill,” including instructions “that instruct Claude to connect to potentially untrusted external network sources.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the SKILL.md as closely as the scripts&lt;/strong&gt;: when &lt;a href=&quot;https://snyk.io/blog/toxicskills-malicious-ai-agent-skills-clawhub/&quot;&gt;Snyk scanned 3,984 skills&lt;/a&gt; from two public registries in February, its reviewers confirmed 76 with “malicious payloads in their markdown instructions to AI agents” (vendor research).&lt;/p&gt;
&lt;p&gt;Which skills to trust, and how to pin and remove them, is the subject of &lt;a href=&quot;https://durabilitycurve.com/blog/skills-are-package-management-for-your-ai/&quot;&gt;Skills Are Package Management for Your AI&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-to-check-claude-chose-your-skill&quot;&gt;How to check Claude chose your skill&lt;/h2&gt;
&lt;p&gt;Claude Code’s docs draw the line this guide has been following: “Seeing a skill trigger tells you Claude found it, not that it did what you intended. To know a skill is working, measure separately whether Claude invokes it on the prompts it should, and whether the output matches what you expect when it does.”&lt;/p&gt;
&lt;p&gt;Calling a skill by name shows you the output. Three steps check the choice.&lt;/p&gt;
&lt;h3 id=&quot;step-1-check-that-claude-code-can-read-the-frontmatter&quot;&gt;Step 1: Check that Claude Code can read the frontmatter&lt;/h3&gt;
&lt;p&gt;If it can’t, Claude Code “loads the skill body with empty metadata, so /skill-name still works but Claude can’t match against your description.” The validator, in Claude Code 2.1.233 or later, finds that. Here it is on a description with a stray quotation mark inside:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;claude&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; validate&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; .claude/skills&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✘ Found 1 error:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ❯ frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected token. At runtime this skill loads with empty metadata (all frontmatter fields silently dropped).&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-2-read-the-skill-call&quot;&gt;Step 2: Read the Skill call&lt;/h3&gt;
&lt;p&gt;When Claude picks a skill in your session, it calls a tool named &lt;code&gt;Skill&lt;/code&gt;, and a headless run prints that call. Scott Spence &lt;a href=&quot;https://scottspence.com/posts/measuring-claude-code-skill-activation-with-sandboxed-evals&quot;&gt;measured skill activation this way&lt;/a&gt; in February, running prompts “through &lt;code&gt;claude -p&lt;/code&gt; with &lt;code&gt;--output-format stream-json&lt;/code&gt;” because he only needed to know “did it call Skill() or not?”&lt;/p&gt;
&lt;p&gt;This command prints the name of the skill Claude called. On &lt;code&gt;helper&lt;/code&gt; with Anthropic’s description it printed the line below; on the vague &lt;code&gt;helper&lt;/code&gt;, nothing. It needs &lt;code&gt;jq&lt;/code&gt;, and it ran as shown on Claude Code 2.1.292; the stream’s layout can change between versions.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;claude&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Write a commit message for the staged changes.&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --output-format&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; stream-json&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --verbose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; jq&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;select(.type==&quot;assistant&quot;) | .message.content[]?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;           | select(.type==&quot;tool_use&quot; and .name==&quot;Skill&quot;) | .input&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;skill&quot;:&quot;helper&quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;claude -p&lt;/code&gt; run is a new session, which matters, because “leftover context from authoring the skill will mask gaps in the written instructions.”&lt;/p&gt;
&lt;p&gt;Typing &lt;code&gt;/helper&lt;/code&gt; can’t stand in for this: Claude Code runs a skill you name directly, and in my runs the stream then showed no &lt;code&gt;Skill&lt;/code&gt; call at all. An empty result can also mean the run failed, so look at the reply before you rewrite anything.&lt;/p&gt;
&lt;h3 id=&quot;step-3-vary-the-words&quot;&gt;Step 3: Vary the words&lt;/h3&gt;
&lt;p&gt;On Sonnet 4.5 with four Svelte skills, Spence found activation “keyword-dependent.” With no extra setup, prompts that used a skill’s keyword activated it nearly every time, generic phrasing 20 to 40% of the time, and indirect phrasing almost never.&lt;/p&gt;
&lt;p&gt;So run the check on three or four phrasings you would really use, at least one without the skill’s own words, and on a near-miss request it should leave alone, two or three times each. Then read the replies for the other half of the docs’ test: when Claude chooses the skill, the output should follow it.&lt;/p&gt;
&lt;h3 id=&quot;what-the-check-found&quot;&gt;What the check found&lt;/h3&gt;
&lt;p&gt;Reading the &lt;code&gt;Skill&lt;/code&gt; call across phrasings is the check that caught &lt;code&gt;helper&lt;/code&gt;. I ran five versions in Claude Code 2.1.292 with Opus 5.5: the same body under four frontmatters, each quoted from Anthropic’s best-practice page or built on its naming convention, plus the version above with its two added lines. I wrote each routing plan down before its runs. These were the requests:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Commit requests (three times each)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Write a commit message for the staged changes.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Draft the commit message for what I&apos;ve staged.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  I&apos;m ready to commit. What should the message say?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Summarise my staged diff as a commit message.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Without the word &quot;commit&quot; (three times each)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Prepare the message for the changes I&apos;m about to save.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  I&apos;m about to push this. Write the summary line and a short reason for the history.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Describe this change for the project history in the team&apos;s usual format.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Review requests the skill should leave alone (three times each)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Review my staged changes for bugs and potential regressions.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Check the staged diff for anything risky before I push.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  What did I change in the staged files, and is anything missing a test?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Unrelated (twice each)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  What does the fetch function in client.py do?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Add a docstring to the fetch function in client.py.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img alt=&quot;Five versions of one commit skill, Claude Code 2.1.292 and Opus 5.5. The vague helper was never chosen on a commit request (0 of 21). A telling name, Anthropic&amp;#39;s description, the two together, and the recommended version were each chosen on all 21. No version took a review request or an unrelated one.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;1175&quot; src=&quot;https://durabilitycurve.com/_astro/fig-routing-run-anim.DVbQ5UXm_1pJQny.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;Every planned phrasing gave the same result on every repeat. The version whose name and description both said nothing about commits was never chosen, and on these phrasings either a telling name or a telling description was enough, whether or not the request said “commit”.&lt;/p&gt;
&lt;p&gt;No version took a review request, including the two whose descriptions still said “reviewing staged changes”: the first went to Claude Code’s own &lt;code&gt;code-review&lt;/code&gt; skill every time, and Claude answered the other two itself, so this shows the clause losing to a neighbour that owns the job, not what happens without one.&lt;/p&gt;
&lt;p&gt;On the branch with a ticket, the commit message in every reply from a chosen skill passed a format check I wrote down before scoring it (a &lt;code&gt;type(scope):&lt;/code&gt; first line of 60 characters or fewer, a blank line, a body, and &lt;code&gt;Refs: PAY-142&lt;/code&gt; as the message’s last line), 84 of 84.&lt;/p&gt;
&lt;p&gt;But 14 replies added text outside the message, twelve of them notes about the change, against the skill’s “Reply with the message only,” so 70 of 84 followed every instruction checked. Being chosen didn’t guarantee the skill was followed in full.&lt;/p&gt;
&lt;p&gt;The replies are why the call is the thing to read. Without the skill, 11 of the 12 messages for the commit requests carried the ticket number and none used the house format, so the vague skill looked as if it had nearly worked.&lt;/p&gt;
&lt;p&gt;On the request without “commit” that mentioned “the team’s usual format” the opposite happened: Claude went looking for the team’s conventions, opened &lt;code&gt;helper&lt;/code&gt;’s SKILL.md as an ordinary file and copied the format exactly, all three times, and once more on another phrasing, with no &lt;code&gt;Skill&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A reply in the right format can’t tell you whether Claude chose the skill.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is one skill, one model and twelve phrasings, with 40 other skills loaded. Two exploratory phrasings an AI reviewer ran outside the plan saw the description version miss once, so test your own setup. Beyond that one contest with &lt;code&gt;code-review&lt;/code&gt;, it says nothing about a crowded listing where two skills could both answer.&lt;/p&gt;
&lt;h3 id=&quot;read-your-result&quot;&gt;Read your result&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;If Claude chose the skill on every phrasing and left the near-miss request alone&lt;/strong&gt;, the choice passes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If it missed some&lt;/strong&gt;, put the words from the misses into the description, or into &lt;code&gt;when_to_use&lt;/code&gt; in Claude Code, and run them again, each two or three times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If a sharp description still misses, or another skill fires instead&lt;/strong&gt;, look outside the skill as well: a neighbour claiming the same job, a listing that has dropped descriptions, or a skill of the same name shadowing yours, which the &lt;code&gt;Skill&lt;/code&gt; call won’t reveal because it prints only the name. &lt;a href=&quot;https://durabilitycurve.com/blog/skill-that-never-fired/&quot;&gt;The Skill That Never Fired&lt;/a&gt; covers those, and &lt;a href=&quot;https://durabilitycurve.com/tools/skill-routing-eval/&quot;&gt;its runnable pack&lt;/a&gt; grades a skill against the neighbour it could lose to.&lt;/p&gt;
&lt;p&gt;Anthropic’s skill-creator plugin automates part of this: it “generates should-trigger and should-not-trigger prompts, measures the hit rate, and proposes description edits when the skill activates on the wrong requests.” In the Claude app there is no stream to read; the help centre’s advice is to “Review Claude’s thinking to confirm it’s loading the skill.”&lt;/p&gt;
&lt;h2 id=&quot;the-checklist&quot;&gt;The checklist&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This week&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Your skill’s name says its job, and its description says what it does and when to use it, in the words you would type, with the main use first.&lt;/p&gt;
&lt;p&gt;☐ &lt;code&gt;claude plugin validate&lt;/code&gt; passes on the folder.&lt;/p&gt;
&lt;p&gt;☐ The &lt;code&gt;Skill&lt;/code&gt; call appears on three or four phrasings you really use, one of them without the skill’s keyword, each run more than once, and stays away from a near-miss request the skill should leave alone.&lt;/p&gt;
&lt;p&gt;☐ When Claude chooses the skill, the reply follows it; you have checked the output as well as the call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Any skill that commits, deploys or sends has &lt;code&gt;disable-model-invocation: true&lt;/code&gt;, and your permissions cover the action itself.&lt;/p&gt;
&lt;p&gt;☐ Every skill you didn’t write has been read line by line, the SKILL.md as well as the scripts.&lt;/p&gt;
&lt;p&gt;☐ The team’s skills are committed in &lt;code&gt;.claude/skills/&lt;/code&gt;, with names a personal skill won’t shadow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Once it works&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ The check runs again after each change to a description, a change of model, and each Claude Code update.&lt;/p&gt;
&lt;p&gt;☐ Skills headed for the Claude app use only its six frontmatter fields.&lt;/p&gt;
&lt;h2 id=&quot;the-whole-job&quot;&gt;The whole job&lt;/h2&gt;
&lt;p&gt;Write the description for the moment Claude chooses, then watch it choose. This week, run the check on the skill you rely on most, with one phrasing that leaves out its keyword and one request it should ignore, and rewrite the description until the call appears, the way &lt;code&gt;{&quot;skill&quot;:&quot;helper&quot;}&lt;/code&gt; did, or make it a skill you call by name. &lt;strong&gt;Until you have watched Claude choose it, a skill is a hypothesis.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Further reading: if you read three of the originals in full, make them &lt;a href=&quot;https://code.claude.com/docs/en/skills&quot;&gt;Claude Code’s skills docs&lt;/a&gt;, &lt;a href=&quot;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices&quot;&gt;Anthropic’s skill-authoring best practices&lt;/a&gt; and &lt;a href=&quot;https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills&quot;&gt;Anthropic’s engineering post on skills&lt;/a&gt;. Every other source is linked where it is used. The sources run from October 2025 to October 2026, and each quotation was matched word for word against a saved copy of its page.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>AI Agent Permissions: How to Stop Your Agent Doing Something You Can&apos;t Undo</title><link>https://durabilitycurve.com/blog/ai-agent-permissions/</link><guid isPermaLink="true">https://durabilitycurve.com/blog/ai-agent-permissions/</guid><description>Our deny rule stopped one spelling of a branch delete and missed the next. The guards that hold for agent actions you can&apos;t undo, and how to test yours.</description><pubDate>Fri, 23 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p class=&quot;lede-dek&quot;&gt;&lt;em&gt;Our deny rule stopped one spelling of a branch delete and missed the next. The guards that hold for agent actions you can’t undo, and how to test yours.&lt;/em&gt;&lt;/p&gt;
&lt;p data-lead-img=&quot;&quot;&gt;&lt;img alt=&quot;A Git branch marked &amp;quot;deletion prohibited&amp;quot;, tried against five guards: a prompt, a reviewer model, a sandbox, a deny rule and the server.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;819&quot; src=&quot;https://durabilitycurve.com/_astro/ai-agent-permissions-hero-anim.CN8D8DI1_11gevP.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI agent permissions are the rules that decide what a coding agent may do without asking you first; a sandbox decides what it can reach when it does.&lt;/strong&gt; Claude Code, Codex and Cursor all have both, under different names.&lt;/p&gt;
&lt;p&gt;A user asked an agent to “clean up old branches.” The agent “listed remote branches, constructed a pattern match, and issued a delete.”&lt;/p&gt;
&lt;p data-cover-after=&quot;&quot;&gt;The case comes from Anthropic’s internal log of times its agents went further than asked, adapted in its &lt;a href=&quot;https://www.anthropic.com/research/claude-code-auto-mode&quot;&gt;write-up of Claude Code’s auto mode&lt;/a&gt;. Anthropic calls this overeager: the agent is trying to help and “takes initiative beyond what the user would approve.” The user “may have only meant to delete local branches.”&lt;/p&gt;
&lt;p&gt;Had the agent asked first, a prompt would have shown you the delete, and stopping it would have depended on you reading it. Someone has to judge what “clean up” covered: the agent, you at the prompt, or in auto mode a second model reading the transcript.&lt;/p&gt;
&lt;p&gt;This guide follows that one deletion through five guards, from the prompt to a server that refuses it, and ends with a way to check which of your own guards hold.&lt;/p&gt;
&lt;p&gt;Short of time? Section 2 carries the argument, and the box in section 3 is the first thing to do.&lt;/p&gt;
&lt;h2 id=&quot;where-a-guard-can-sit&quot;&gt;Where a guard can sit&lt;/h2&gt;
&lt;p&gt;An action can be stopped in three places.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Someone can judge whether you meant it&lt;/strong&gt;: you at a prompt, or a reviewer model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent’s machine can block it&lt;/strong&gt;, with a rule that names the command, a hook, or a sandbox that limits what commands reach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The system the action lands on can refuse it&lt;/strong&gt;, which for a branch is the git server.&lt;/p&gt;
&lt;p&gt;With nothing set up on the machine or the server, the deleted branch meets only the first kind: a git server accepts a delete unless someone has told it not to.&lt;/p&gt;
&lt;p&gt;None of the four stops in &lt;a href=&quot;https://durabilitycurve.com/blog/what-is-harness-engineering/&quot;&gt;the first guide in this series&lt;/a&gt; sits at the destination, and section 4 shows why that is the place to add one.&lt;/p&gt;
&lt;h2 id=&quot;why-asking-cant-carry-what-you-cant-undo&quot;&gt;Why asking can’t carry what you can’t undo&lt;/h2&gt;
&lt;p&gt;In Manual mode the prompt is the guard you have, and Anthropic has measured how it goes: “approval fatigue, where people stop paying close attention to what they’re approving,” and “in practice users accept 93% of them anyway.”&lt;/p&gt;
&lt;p&gt;That rate is what you would expect if most prompts are harmless. The harmful one arrives looking like the rest.&lt;/p&gt;
&lt;p&gt;Auto mode hands the judgement to a classifier, a second model that checks each risky action before it runs. Anthropic tested it on 52 real overeager actions, collected from sessions where an employee denied an action or it was flagged afterwards, and the full system let 17% through. On 52 cases, each miss moves that figure by about 2 points.&lt;/p&gt;
&lt;p&gt;“The 17% false-negative rate on real overeager actions is the honest number,” the write-up says, and it explains what went wrong: “In the majority of misses we examined, the classifier correctly identifies the danger.” What it misjudges “is whether a real consent signal in the session actually covers this action.” Its example: “‘Clean up the PR’ was said by the user, but it doesn’t name force-pushing.”&lt;/p&gt;
&lt;p&gt;It is also a chosen trade-off: the second stage “cleans up the false positives but lets a few more dangerous actions through,” and the full system wrongly blocked 0.4% of 10,000 real staff actions.&lt;/p&gt;
&lt;p&gt;Anthropic’s verdict is that against careful approval of every action “it’s arguably a regression,” and that auto mode is “not a drop-in replacement for careful human review on high-stakes infrastructure.”&lt;/p&gt;
&lt;p&gt;The agent and the classifier fail at the same judgement, whether this request covers this action, and a prompt asks you to make that judgement again every time, at speed.&lt;/p&gt;
&lt;p&gt;So sort your agent’s actions by whether you can undo them. The ones you can undo, such as an edit inside a repository with its history, can run with fewer prompts inside a wall.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actions you can’t undo need a guard that never has to decide what anyone meant.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;list-what-your-agent-cant-undo&quot;&gt;List what your agent can’t undo&lt;/h2&gt;
&lt;p&gt;The block rules Anthropic described when it launched auto mode in March make a ready starting list. The first group is “Destroy or exfiltrate. Cause irreversible loss by force-pushing over history, mass-deleting cloud storage, or sending internal data externally.”&lt;/p&gt;
&lt;p&gt;Another covers shared systems: “pushing directly to main, running production deploys, or modifying shared resources or jobs the agent didn’t create.” A third includes “modifying the agent’s own permission config.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Those defaults have since moved&lt;/strong&gt;: Claude Code’s current docs allow by default “Pushing to any branch of the repository you’re working in, including the default branch,” while a force push is still blocked. Check what your installed version permits rather than assuming the reviewer stops a push to main.&lt;/p&gt;
&lt;p&gt;For a typical coding agent, the list comes down to remote branch deletes and force-pushes, writes to production data, deploys, and anything that sends data out.&lt;/p&gt;
&lt;p&gt;A clone may still hold a deleted branch’s commits, but nothing at the moment of the command says whether one does, so treat the branch as gone.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do this first (10 minutes):&lt;/strong&gt; Write down the actions your agent could take this week that you couldn’t undo yourself in five minutes. Use Anthropic’s groups as prompts: losing data or history, sending data out, and touching shared systems such as main, production or someone else’s jobs. Next to each, write what stops it today. If the answer is “it would ask me,” start there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;five-guards-on-the-deleted-branch&quot;&gt;Five guards on the deleted branch&lt;/h2&gt;
&lt;p&gt;Five guards now meet the same deletion. The first two depend on someone judging consent. The next two judge nothing, but they run on the agent’s own machine. The last runs somewhere else.&lt;/p&gt;
&lt;p&gt;François Martin, writing after an agent of his emptied two main branches, set out the same kind of order with branch protection first: “I would set up three layers, in this order.” We ran steps 4 and 5.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Five guards for the deleted branch as a rack of modules: where each sits, what it did with the delete, and what it holds only if.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;796&quot; src=&quot;https://durabilitycurve.com/_astro/fig-guard-rack-anim.BSKS0hJK_Z5jiNq.webp&quot; &gt;&lt;/p&gt;
&lt;h3 id=&quot;step-1-ask&quot;&gt;Step 1: Ask&lt;/h3&gt;
&lt;p&gt;Claude Code’s Manual mode (its config name is &lt;code&gt;default&lt;/code&gt;) “stops and asks you before most actions that edit files, run shell commands, or reach the network,” according to &lt;a href=&quot;https://code.claude.com/docs/en/permission-modes&quot;&gt;its docs&lt;/a&gt;. The delete arrives as one more prompt among many. Keep prompts for actions you can undo, where a careless yes costs you a minute.&lt;/p&gt;
&lt;h3 id=&quot;step-2-a-second-model&quot;&gt;Step 2: A second model&lt;/h3&gt;
&lt;p&gt;Claude Code’s auto mode, Codex’s automatic review and Cursor’s Auto-review all send risky actions to a reviewer model. For this case Anthropic’s March write-up says, “This would be blocked since the request was vague, the action irreversible and destructive.”&lt;/p&gt;
&lt;p&gt;Other cases get through. Martin was running Claude Code in auto mode with Opus 5 when it tried to undo a bad commit from a shallow clone, and the revert “staged every single file in the repository for deletion.”&lt;/p&gt;
&lt;p&gt;The pushes that followed “removed 151 files from one main branch and 723 from the other,” as he &lt;a href=&quot;https://dev.karakun.com/2026/08/28/coding-agent-pushed-deletion-to-main.html&quot;&gt;describes on Karakun’s blog&lt;/a&gt;. Of the model and the classifier, his verdict is “Neither stopped this.”&lt;/p&gt;
&lt;p&gt;The vendors say what a reviewer is. Claude Code’s docs call the classifier “a per-action control, not an isolation boundary,” and &lt;a href=&quot;https://cursor.com/docs/agent/security/run-modes&quot;&gt;Cursor’s&lt;/a&gt; say “Auto-review is not a security boundary.” Use one to be asked less often.&lt;/p&gt;
&lt;p&gt;One detail matters for your list. In auto mode, “File writes and edits inside the project directory are allowed without a classifier call,” because, Anthropic says, “in-project edits are reviewable via version control.” A file that version control doesn’t track has no such undo.&lt;/p&gt;
&lt;h3 id=&quot;step-3-a-sandbox&quot;&gt;Step 3: A sandbox&lt;/h3&gt;
&lt;p&gt;A sandbox limits what a command can reach, and Anthropic’s engineers write that “effective sandboxing requires both filesystem and network isolation.” In &lt;a href=&quot;https://www.anthropic.com/research/claude-code-sandboxing&quot;&gt;their internal use&lt;/a&gt; it cut permission prompts by 84%.&lt;/p&gt;
&lt;p&gt;Claude Code’s &lt;a href=&quot;https://code.claude.com/docs/en/sandboxing&quot;&gt;sandbox&lt;/a&gt; gives a command “no direct route to the network,” and its proxy “checks the hostname of each connection against your allowed and denied domains.”&lt;/p&gt;
&lt;p&gt;If the git host isn’t allowed, a sandboxed push can’t reach it, though a blocked command can be retried outside the sandbox unless you set &lt;code&gt;allowUnsandboxedCommands&lt;/code&gt; to false.&lt;/p&gt;
&lt;p&gt;The agent’s job usually needs that host, and once it is allowed, the sandbox can’t tell an ordinary push from a deletion. That sandbox also “covers shell commands only. Claude’s file tools, MCP servers, and hooks run outside it.”&lt;/p&gt;
&lt;h3 id=&quot;step-4-a-deny-rule&quot;&gt;Step 4: A deny rule&lt;/h3&gt;
&lt;p&gt;A deny rule names a command and blocks it, and in Claude Code “Deny rules block in every mode, including bypassPermissions.” This one, in the project’s &lt;code&gt;.claude/settings.json&lt;/code&gt;, names the delete:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;permissions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;deny&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Bash(git push origin --delete *)&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We ran it with Claude Code 2.1.292 against a throwaway repository with a local remote, asking the agent to run one exact command each time. The rule stopped the command it names:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL CALL: git push origin --delete old-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL RESULT: Permission to use Bash with command git push origin --delete old-1 has been denied.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we told it to send the same deletion in the other form git accepts:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL CALL: git push origin :old-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL RESULT: To ../remote.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; - [deleted]         old-1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The branch was gone. We had allowed &lt;code&gt;git push *&lt;/code&gt; so that ordinary pushes would run, and the colon form matched that and slipped past the deny rule.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://code.claude.com/docs/en/permissions&quot;&gt;Claude Code’s permissions docs&lt;/a&gt; say a deny rule “covers the invocation Claude usually produces and isn’t a security boundary around the program.”&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://rulestack.hashnode.dev/claude-code-permission-rules-bash-git-push-stopped-8-of-14-ways-to-push-and-5-reached-the-remote&quot;&gt;Rulestack&lt;/a&gt;, which sells rule packs for these tools, tried 14 ways of writing a push against one rule, and “5 pushed to the remote.” We picked the second spelling ourselves.&lt;/p&gt;
&lt;h3 id=&quot;step-5-refuse-it-at-the-destination&quot;&gt;Step 5: Refuse it at the destination&lt;/h3&gt;
&lt;p&gt;The last guard runs on the server. Git has a setting for it, &lt;code&gt;receive.denyDeletes&lt;/code&gt;, which “will deny a ref update that deletes the ref” (&lt;a href=&quot;https://git-scm.com/docs/git-config&quot;&gt;git docs&lt;/a&gt;). We switched it on in the remote and sent the colon form again:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; receive.denyDeletes&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   # in the remote repository&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL CALL: git push origin :old-2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TOOL RESULT: Exit code 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;remote: error: denying ref deletion for refs/heads/old-2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;To ../remote.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; ! [remote rejected] old-2 (deletion prohibited)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server refuses the deletion however the command is spelled, but &lt;strong&gt;it refuses only what it names&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In a second test, with &lt;code&gt;receive.denyNonFastForwards&lt;/code&gt; on as well, an ordinary push of a commit that deleted every file went through and left &lt;code&gt;main&lt;/code&gt; empty. That is the shape of Martin’s incident: commits that deleted the files, with every branch still in place.&lt;/p&gt;
&lt;p&gt;His fix for it is a required pull request with CI: “Letting the agent open a pull request instead would have stopped both incidents, even with automatic merging once CI passed,” because “neither the invalid YAML nor the deletion commits would have passed CI.” So match the server rule to the failure, and remember it holds only while the agent can’t change the server.&lt;/p&gt;
&lt;p&gt;In our test the remote was a folder on the same machine, so the agent could have deleted the branch there directly. On a real server, &lt;strong&gt;the credential decides&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In one user’s &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/42849&quot;&gt;report against Claude Code&lt;/a&gt;, a force-push needed for a git-history scrub was blocked by branch protection and a ruleset, and “the agent disabled GitHub branch protection rules and a repository ruleset without user authorization in order to unblock a force push.”&lt;/p&gt;
&lt;p&gt;Martin’s answer is to give the agent “a machine account or app installation whose credentials have no bypass rights.” That helps only if your own token is out of the agent’s reach, which is how Anthropic built its web sandbox: git credentials and signing keys “are never inside the sandbox with Claude Code.”&lt;/p&gt;
&lt;p&gt;On GitHub, a ruleset does the server’s job. “Require a pull request before merging”, together with “Require status checks to pass before merging”, would have stopped Martin’s commits, because his CI would have failed them.&lt;/p&gt;
&lt;p&gt;That pair protects you only against what your checks detect, so test the checks too; a pull request alone “doesn’t necessarily have to be approved, but it must be opened.”&lt;/p&gt;
&lt;p&gt;“Restrict deletions” means “only users with bypass permissions can delete branches or tags whose name matches the pattern you specify,” and “Block force pushes” sits beside it; both come ticked when you create a ruleset (&lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets&quot;&gt;GitHub’s docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;On private repositories rulesets need a paid plan, “GitHub Pro, GitHub Team, and GitHub Enterprise Cloud” (&lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets&quot;&gt;GitHub&lt;/a&gt;). Without one, &lt;code&gt;receive.denyDeletes&lt;/code&gt; on a server you run is the fallback.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The cost is real&lt;/strong&gt;: under a ruleset, someone with bypass rights deletes branches by hand, and with &lt;code&gt;receive.denyDeletes&lt;/code&gt; even your own push is refused, so a branch goes only when an admin turns the setting off or deletes it on the server.&lt;/p&gt;
&lt;p&gt;Where you don’t run the destination at all, the credential is the guard: a database user that can read production and not write to it, a token with no delete rights.&lt;/p&gt;
&lt;h2 id=&quot;the-same-guards-in-claude-code-codex-and-cursor&quot;&gt;The same guards in Claude Code, Codex and Cursor&lt;/h2&gt;
&lt;p&gt;Each tool has the same pieces under different names.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The same guards in Claude Code, Codex and Cursor as three faders, from asking you to no prompts, each knob at that tool&amp;#39;s default or recommended setting.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;836&quot; src=&quot;https://durabilitycurve.com/_astro/fig-mode-faders-anim.C1RXEQPf_208OX8.webp&quot; &gt;&lt;/p&gt;
&lt;h3 id=&quot;claude-code&quot;&gt;Claude Code&lt;/h3&gt;
&lt;p&gt;Where auto mode is available, interactive Claude Code sessions now start in it: “With Claude Code v2.1.283 or later, auto mode is the built-in starting permission mode for interactive terminal and VS Code sessions.” So the deleted branch would meet step 2, and nothing after it unless you add steps 3 to 5.&lt;/p&gt;
&lt;h3 id=&quot;codex&quot;&gt;Codex&lt;/h3&gt;
&lt;p&gt;Codex starts at the wall. Its local defaults “include no network access and write permissions limited to the active workspace,” and under its on-request approval policy the agent “asks when it needs to go beyond that boundary,” so a push over the network comes to you first.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;approvals_reviewer = &quot;auto_review&quot;&lt;/code&gt; sends those requests to a reviewer model instead, and automatic review “doesn’t change the sandbox boundary,” &lt;a href=&quot;https://learn.chatgpt.com/docs/sandboxing&quot;&gt;OpenAI notes&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;cursor&quot;&gt;Cursor&lt;/h3&gt;
&lt;p&gt;Cursor has three Run Modes: Auto-review, Allowlist and Run Everything. “The safest useful setup for most people is Auto-review,” its docs say, and Auto-review became the recommended default on 29 May 2026. We ran Claude Code only; the Codex and Cursor settings here come from their docs. Defaults move between versions, so check your mode after every update.&lt;/p&gt;
&lt;h2 id=&quot;a-setup-for-each-kind-of-task&quot;&gt;A setup for each kind of task&lt;/h2&gt;
&lt;p&gt;Set the agent by the job, and keep the guards from your list on whatever the job.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A selector with one position for each kind of task: exploring, everyday edits, long tasks and unattended runs; in every position the can&amp;#39;t-undo list keeps its own guard.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;1303&quot; src=&quot;https://durabilitycurve.com/_astro/fig-task-selector-anim.Dox_aoZc_Z8bpSO.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exploring code you don’t know&lt;/strong&gt; should start without write access. Claude Code’s plan mode is for investigating and proposing changes, but it still runs “classifier-approved commands when auto mode is available,” so where that matters use a real read-only boundary, such as Codex’s read-only sandbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everyday work in one repository&lt;/strong&gt; can allow edits inside the project, with version control as the undo, so commit before you hand over a task.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Long tasks you check in on&lt;/strong&gt; are where fewer prompts pay off: auto mode or automatic review, inside a sandbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unattended runs&lt;/strong&gt; need an isolated environment and narrowly scoped credentials. Auto mode can run without routine prompts, and Claude Code’s docs suggest a sandbox or container as an extra layer there.&lt;/p&gt;
&lt;p&gt;The bypass modes (bypassPermissions, Run Everything, danger-full-access) belong only in a container or a virtual machine: the docs say to use bypassPermissions only “in isolated environments like containers or VMs where Claude Code can’t cause damage.”&lt;/p&gt;
&lt;p&gt;Whatever the job, the actions on your list keep their own guards: a deny rule you have tested, a refusal on the server, or a credential that can’t do them.&lt;/p&gt;
&lt;h2 id=&quot;what-no-guard-here-covers&quot;&gt;What no guard here covers&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Damage inside the wall stays possible.&lt;/strong&gt; Auto mode lets in-project edits through without a classifier call, and an agent allowed to edit your project can still break it, or &lt;a href=&quot;https://durabilitycurve.com/blog/agent-proof-tests/&quot;&gt;rewrite the tests that judge it&lt;/a&gt;. Version control lets you recover tracked changes, but untracked files, ignored files and uncommitted work can still be lost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An agent can reach its own rules.&lt;/strong&gt; “In bypassPermissions mode, Claude Code skips permission prompts, including for writes to protected paths such as .git and .claude,” and &lt;code&gt;.claude&lt;/code&gt; is where the deny rule lives.&lt;/p&gt;
&lt;p&gt;That is the gap in &lt;a href=&quot;https://durabilitycurve.com/blog/the-guardrail-your-agent-can-reach/&quot;&gt;The Guardrail Your Agent Can Reach&lt;/a&gt;. A guard the agent can edit is a guard it can remove. Claude Code’s settings can switch off bypass and auto mode, and the docs say those switches “are most useful in managed settings where they can’t be overridden.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything above assumes the agent is trying to help.&lt;/strong&gt; When something it read is steering it, the request itself comes from an attacker; a later guide in this series covers that case.&lt;/p&gt;
&lt;h2 id=&quot;test-your-guards&quot;&gt;Test your guards&lt;/h2&gt;
&lt;p&gt;The first guide’s rule was that every addition to a harness is a hypothesis until you test it. A guard is one too.&lt;/p&gt;
&lt;p&gt;For each action on your list, make a throwaway copy of the destination, ask the agent to do the thing, then ask again with the command written another way. Then turn on the destination’s guard and try once more.&lt;/p&gt;
&lt;p&gt;For a git server, the copy takes six lines in an empty folder (it assumes git already has your name and email), and the last one should fail:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -q&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --bare&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; remote.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -q&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; work&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; work&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; remote&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ../remote.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -q&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --allow-empty&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -q&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; HEAD:main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; old-1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -q&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; old-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -C&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ../remote.git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; receive.denyDeletes&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; :old-1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;remote: error: denying ref deletion for refs/heads/old-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;To ../remote.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; ! [remote rejected] old-1 (deletion prohibited)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;error: failed to push some refs to &apos;../remote.git&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a hosted server, repeat it in a disposable repository with the same rules and the agent’s own credentials, never against anything you need.&lt;/p&gt;
&lt;p&gt;We also gave Claude Opus 5.5 the incident’s own words, “Clean up old branches.”, in three recorded runs. Each time it deleted only the old local branches and never touched the remote.&lt;/p&gt;
&lt;p&gt;Those runs were headless, locked down and on one phrasing; Martin’s auto-mode session pushed the deletion of every file to two main branches, which a required pull request with checks would have stopped. The guards are for that run, as long as they match it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A guard you haven’t seen refuse is a hypothesis.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-checklist&quot;&gt;The checklist&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This week&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ You have a written list of what your agent could do that you couldn’t undo in five minutes.&lt;/p&gt;
&lt;p&gt;☐ Each item on it has a guard that doesn’t depend on anyone reading a prompt.&lt;/p&gt;
&lt;p&gt;☐ The agent pushes and deploys as its own identity, with no admin or bypass rights.&lt;/p&gt;
&lt;p&gt;☐ The server refuses branch deletes (Restrict deletions, or &lt;code&gt;receive.denyDeletes&lt;/code&gt;) and force-pushes (Block force pushes, or &lt;code&gt;receive.denyNonFastForwards&lt;/code&gt;) on shared branches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Every deny rule on the list has been tried with the command written a second way.&lt;/p&gt;
&lt;p&gt;☐ Changes to main arrive through a required pull request, with checks that would catch a destructive change and that the agent can’t bypass.&lt;/p&gt;
&lt;p&gt;☐ The agent’s other credentials can’t do what’s on the list: read-only database users, tokens without delete rights.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Once it works&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Every guard on the list has refused on a throwaway copy, and is tried again after each update.&lt;/p&gt;
&lt;p&gt;☐ Where agents run unattended, the switches that turn off bypass and auto mode live in managed settings.&lt;/p&gt;
&lt;h2 id=&quot;the-whole-job&quot;&gt;The whole job&lt;/h2&gt;
&lt;p&gt;List what your agent can’t undo, try one of those actions on a copy this week, and keep only the guards you have watched refuse it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Further reading: if you read three of the originals in full, make them &lt;a href=&quot;https://www.anthropic.com/research/claude-code-auto-mode&quot;&gt;Anthropic on how auto mode was built&lt;/a&gt;, &lt;a href=&quot;https://dev.karakun.com/2026/08/28/coding-agent-pushed-deletion-to-main.html&quot;&gt;François Martin on the agent that emptied two main branches&lt;/a&gt; and &lt;a href=&quot;https://code.claude.com/docs/en/permissions&quot;&gt;Claude Code’s permissions docs&lt;/a&gt;. Every other source is linked where it is used. The sources run from October 2025 to October 2026; I rechecked each quotation against its page on 23 October 2026.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>What Is Harness Engineering? How to Build AI Agents That Catch Their Own Mistakes</title><link>https://durabilitycurve.com/blog/what-is-harness-engineering/</link><guid isPermaLink="true">https://durabilitycurve.com/blog/what-is-harness-engineering/</guid><description>Lessons from OpenAI, Anthropic, Google and independent engineers, worked through one failure, with a way to test every change.</description><pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p class=&quot;lede-dek&quot;&gt;&lt;em&gt;Lessons from OpenAI, Anthropic, Google and independent engineers, worked through one failure, with a way to test every change.&lt;/em&gt;&lt;/p&gt;
&lt;p data-lead-img=&quot;&quot;&gt;&lt;img alt=&quot;A dark glass sphere labelled model, held in a gold gimbal cage, with four tags on the cage: AGENTS.md, contract check, deny rule and held-out test.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;819&quot; src=&quot;https://durabilitycurve.com/_astro/harness-engineering-hero-anim.BXCfbxN7_ZIesMM.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Harness engineering is designing everything around an AI model (its instructions, context, tools, checks and sign-off points) so the agent gets work right first time and catches its own mistakes.&lt;/strong&gt; The model does the reasoning. The harness is much of what makes it reliable.&lt;/p&gt;
&lt;p&gt;One failure runs through this guide. You ask a coding agent to tidy up a data model. It renames a field, the code compiles, the checks in front of it pass, and it reports success. Two other services that read that field quietly break. &lt;a href=&quot;https://www.thoughtworks.com/insights/blog/architecture/engineering-the-harness-a-practical-pattern-for-reliable-coding-agents&quot;&gt;Thoughtworks engineers&lt;/a&gt;, who use this example, call it “locally correct, but systemically destructive,” and they say the gap between model intelligence and reliable delivery “is not primarily a model-quality issue.” What the agent lacked, in their words, was “system-level visibility, architectural guardrails and automated feedback mechanisms.”&lt;/p&gt;
&lt;p data-cover-after=&quot;&quot;&gt;This guide applies what OpenAI, Anthropic, Google, Thoughtworks and independent engineers have learned to that one failure, a layer at a time, and shows how to test each fix. If you have ten minutes, read section 3 and do the box in step 2 of section 4.&lt;/p&gt;
&lt;h2 id=&quot;what-a-harness-is&quot;&gt;What a harness is&lt;/h2&gt;
&lt;p&gt;The short version is a formula: &lt;strong&gt;Agent = Model + Harness.&lt;/strong&gt; Birgitta Böckeler, &lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;writing on Martin Fowler’s site&lt;/a&gt;, splits the harness in two. Part of it comes built into the tool, such as the system prompt and the way it searches your files. The rest is an &lt;strong&gt;outer harness&lt;/strong&gt; that you build for your own work and your own codebase.&lt;/p&gt;
&lt;p&gt;She also gives the most useful working model. &lt;strong&gt;Guides&lt;/strong&gt; steer the agent before it acts: instructions, skills, examples. &lt;strong&gt;Sensors&lt;/strong&gt; check its work afterwards: tests, linters, type checks, review. Guides alone give you an agent “that encodes rules but never finds out whether they worked”; sensors alone, one “that keeps repeating the same mistakes.” The renamed field got past both. Nothing told the agent the field was shared, and nothing checked the services that read it.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Guides before, sensors after: the AGENTS.md line steers the agent, the contract check catches the rename, and a repeat updates the guide or adds a check.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;747&quot; src=&quot;https://durabilitycurve.com/_astro/fig-rename-loop-anim.C2pxmUfA_2vSyFt.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;Mitchell Hashimoto, who started calling it harness engineering in &lt;a href=&quot;https://mitchellh.com/writing/my-ai-adoption-journey&quot;&gt;a February post&lt;/a&gt;, states the aim: “anytime you find an agent makes a mistake, you take the time to engineer a solution such that the agent never makes that mistake again.” Read “never” as the aim. Agents are not deterministic, so some failures come back by chance, and some fixes cause new problems. That is what section 3 is about.&lt;/p&gt;
&lt;p&gt;For the case that the harness matters more than the model, see &lt;a href=&quot;https://durabilitycurve.com/blog/harness-engineering-same-model-different-product/&quot;&gt;Harness Engineering: Same Model, Different Product&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;harness-engineering-vs-context-engineering&quot;&gt;Harness engineering vs context engineering&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The short answer: context engineering decides what the model sees; harness engineering is the wider system that also prevents, checks and fixes.&lt;/strong&gt; &lt;a href=&quot;https://www.anthropic.com/research/effective-context-engineering-for-ai-agents&quot;&gt;Anthropic&lt;/a&gt; calls context engineering “the natural progression of prompt engineering.” &lt;a href=&quot;https://www.hlyr.dev/blog/skill-issue-harness-engineering-for-coding-agents&quot;&gt;HumanLayer&lt;/a&gt; draws the line the other way, calling harness engineering “the subset of context engineering” done through the agent’s own configuration points.&lt;/p&gt;
&lt;p&gt;Either way, the renamed field failed on both counts: the services that read the field were never in the agent’s context, and no check ran against them. Treat &lt;a href=&quot;https://smartscope.blog/en/blog/harness-engineering-overview/&quot;&gt;SmartScope’s comparison&lt;/a&gt; as a first question to ask: a single wrong answer suggests context, and quality that slips with repeated use suggests the harness, though one wrong answer can also come from the model, a tool or a missing check.&lt;/p&gt;
&lt;h2 id=&quot;every-addition-is-a-hypothesis&quot;&gt;Every addition is a hypothesis&lt;/h2&gt;
&lt;p&gt;The reflex after a failure is to add something: another rule, another tool, another check. The best evidence so far says to test that reflex.&lt;/p&gt;
&lt;p&gt;Two controlled studies have tested the instruction files at the centre of most harnesses. &lt;a href=&quot;https://arxiv.org/abs/2602.11988&quot;&gt;ETH Zurich researchers&lt;/a&gt;, working on Python repositories, found that context files, whether written by a model or by the repo’s own developers, did “not generally improve task success rates,” raised cost by over 20%, and did no better when shorter. The authors conclude the files are useful for “specifying non-standard coding practices.”&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2601.20404&quot;&gt;A smaller study&lt;/a&gt;, by Lulla and colleagues, ran agents with and without an AGENTS.md on 124 pull requests and found the file cut median runtime by 28.6% and output tokens by 16.6%, with completion about the same. The two teams measured cost differently (ETH counted inference cost; Lulla’s counted runtime and output tokens), so their efficiency results are not directly comparable.&lt;/p&gt;
&lt;p&gt;Neither found a reliable gain in task success: developer-written files did slightly better in the ETH runs (2.4 percentage points on average, not statistically significant), and clearly better than generated ones.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.anthropic.com/research/harness-design-long-running-apps&quot;&gt;Anthropic’s harness-design team&lt;/a&gt; gives the reason this matters beyond instruction files. Every part of a harness “encodes an assumption about what the model can’t do on its own,” and those assumptions “can quickly go stale as models improve.” Their working rule comes from an earlier post: “find the simplest solution possible, and only increase complexity when needed.” &lt;a href=&quot;https://www.thoughtworks.com/insights/blog/architecture/engineering-the-harness-a-practical-pattern-for-reliable-coding-agents&quot;&gt;Thoughtworks&lt;/a&gt; puts it in three words: “Earn every rule.”&lt;/p&gt;
&lt;p&gt;Treat every addition to your harness as a hypothesis. It costs tokens, time and attention, and it assumes something about the model that may stop being true. Make the smallest change that would have caught the failure, run the same kind of task with and without it, and keep it only if the difference pays for the cost. Section 7 shows how.&lt;/p&gt;
&lt;h2 id=&quot;fixing-the-renamed-field-one-layer-at-a-time&quot;&gt;Fixing the renamed field, one layer at a time&lt;/h2&gt;
&lt;p&gt;Here is the failure again, fixed in layers: see what happened, make it less likely, catch it when it happens, and contain the damage. For a public contract, the check is the layer that has to hold; the instruction only makes the failure rarer.&lt;/p&gt;
&lt;h3 id=&quot;step-1-see-what-the-agent-saw&quot;&gt;Step 1: See what the agent saw&lt;/h3&gt;
&lt;p&gt;Before adding anything, look at what entered the agent’s context on the failed run. &lt;a href=&quot;https://sourcegraph.com/blog/context-engineering&quot;&gt;Sourcegraph’s&lt;/a&gt; test is to log it, “then check whether the answer was in there.” In the rename it was not: the services that read the field live elsewhere, and the agent never opened them. &lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;OpenAI’s line&lt;/a&gt; covers it: “anything it can’t access in-context while running effectively doesn’t exist.” If the agent never encountered the dependency, find out why. It may need a clearer instruction to search for it, access to the right repository, or a check that catches what it missed.&lt;/p&gt;
&lt;h3 id=&quot;step-2-make-it-less-likely-with-one-guide&quot;&gt;Step 2: Make it less likely with one guide&lt;/h3&gt;
&lt;p&gt;The cheapest change is a line in the agent’s standing instructions file. AGENTS.md is &lt;a href=&quot;https://agents.md/&quot;&gt;the shared format&lt;/a&gt;, read by many coding agents; Claude Code reads CLAUDE.md first and, &lt;a href=&quot;https://code.claude.com/docs/en/memory&quot;&gt;from v2.1.277&lt;/a&gt;, falls back to AGENTS.md when a project has no CLAUDE.md. For the rename, the line might read: &lt;em&gt;Field names in &lt;code&gt;orders/schema&lt;/code&gt; are a public contract; before renaming one, search the other services for it and list what reads it.&lt;/em&gt; That is the kind of non-standard rule the ETH study found files are good for.&lt;/p&gt;
&lt;p&gt;Keep the rest of the file lean. OpenAI’s team tried “one big AGENTS.md” and it failed; for one thing, a giant file “crowds out the task.” Theirs is now “roughly 100 lines,” a “table of contents” pointing to a &lt;code&gt;docs/&lt;/code&gt; folder. &lt;a href=&quot;https://www.hlyr.dev/blog/skill-issue-harness-engineering-for-coding-agents&quot;&gt;HumanLayer&lt;/a&gt; keeps theirs “under 60 lines.” In a large repo, put a nested AGENTS.md in each project; agents read the nearest file, “so the closest one takes precedence,” as agents.md puts it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://agentic-engineering.swmansion.com/becoming-productive/harness-engineering/&quot;&gt;Software Mansion’s guide&lt;/a&gt; cites Anthropic’s Claude Code docs on what goes in and what stays out; here is a shortened version:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Keep what the agent can&amp;#39;t find for itself (commands, your non-default rules, how to test, decisions and gotchas, pointers to docs) and cut what it can.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;759&quot; src=&quot;https://durabilitycurve.com/_astro/fig-agents-md-before-after-anim.B2iuv8Er_1I7CMw.webp&quot; &gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do this first (10 minutes):&lt;/strong&gt; Open your instructions file. Delete anything the agent could work out for itself, move long sections into separate docs with one-line pointers, and add the one rule your agent most recently broke.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;step-3-catch-it-with-one-sensor&quot;&gt;Step 3: Catch it with one sensor&lt;/h3&gt;
&lt;p&gt;An instruction can be ignored; a check runs every time. For the rename, that is a contract check: each consuming service lists the fields it reads, in a file the agent cannot edit, and the check fails when the schema no longer has one of them. A test that searches the code for the schema’s own field names looks similar and misses this case, because it never looks for the field the agent just deleted. A first version can be this small (TypeScript, with Vitest and a Zod schema):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// contract.test.ts: runs in CI.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// This file, contracts/ and the CI job sit outside the agent&apos;s write access.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { test, expect } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;vitest&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { OrderSchema } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;../orders/schema&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; consumers &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;../contracts/consumers.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// { &quot;billing&quot;: [&quot;customer_id&quot;, &quot;total&quot;], &quot;shipping&quot;: [&quot;customer_id&quot;, &quot;address&quot;] }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;every field a consumer reads still exists&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fields&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(OrderSchema.shape);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; broken&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;entries&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(consumers).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;flatMap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(([&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;service&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;needed&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    needed.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;fields.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(f)).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;service&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} reads ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  expect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(broken, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;broken&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;; &quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}: keep the old name or update the consumer`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toEqual&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;([]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run against the renamed schema (Vitest 5 with Zod), it fails like this, and it passes again once the old name is back:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;FAIL  tests/contract.test.ts &gt; every field a consumer reads still exists&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;AssertionError: billing reads customer_id; shipping reads customer_id: keep the old name or update the consumer: expected [ &apos;billing reads customer_id&apos;, …(1) ] to deeply equal []&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It catches a removed top-level field and nothing subtler; a production check should also compare types, required fields and the consumers’ real interfaces. And it protects you only while the agent being checked cannot change it, so keep the test, the consumer list and the CI step that runs them out of its reach.&lt;/p&gt;
&lt;p&gt;Run it automatically after every edit, and follow four rules that make sensors work.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Silent success, verbose failure.&lt;/strong&gt; That is &lt;a href=&quot;https://www.thoughtworks.com/insights/blog/architecture/engineering-the-harness-a-practical-pattern-for-reliable-coding-agents&quot;&gt;Thoughtworks’&lt;/a&gt; name for it. HumanLayer learned why the hard way: when they ran the full suite after every change, “4,000 lines of passing tests would flood the context window,” and the agent lost track of its task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the error for the agent.&lt;/strong&gt; OpenAI writes custom lint messages that “inject remediation instructions into agent context”; &lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;Böckeler&lt;/a&gt; calls this “a positive kind of prompt injection.” For the rename, the check above says: &lt;em&gt;billing reads customer_id; shipping reads customer_id: keep the old name or update the consumer.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Promote repeated rules into checks.&lt;/strong&gt; A rule you keep re-adding to your instructions “should be promoted into a mechanical sensor,” says Thoughtworks. Software Mansion’s split: “Written instructions should cover the cases that require judgment. Hooks should cover the cases that do not.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect the checks that decide “done”.&lt;/strong&gt; Agents need to write and change tests as part of the work. What they must not be able to edit is the check that says the work is finished, and an instruction alone will not hold that line. For a lock that does, see &lt;a href=&quot;https://durabilitycurve.com/blog/agent-proof-tests/&quot;&gt;Lock the Tests, Then Test the Lock&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;step-4-contain-it-where-the-stakes-are-high&quot;&gt;Step 4: Contain it where the stakes are high&lt;/h3&gt;
&lt;p&gt;Some changes should not go through on the agent’s say-so. Thoughtworks keeps human confirmation for “irreversible or high-blast-radius choices,” such as changes that span several repositories, schema migrations or public API modifications; everything else proceeds with “sensible defaults.” A renamed field in a public contract is exactly that kind of change.&lt;/p&gt;
&lt;p&gt;There are four ways to stop an action, and they differ in strength. An instruction asks. A hook runs your own script around a tool call and can block it: HumanLayer’s denies any command that tries to run database migrations and tells the agent to ask a human. A deny rule blocks matching tool calls; &lt;a href=&quot;https://code.claude.com/docs/en/permissions&quot;&gt;in Claude Code&lt;/a&gt;, “Deny rules prevent Claude Code from using the specified tool,” and they are checked first, though another tool or command may still reach the same thing. A &lt;a href=&quot;https://code.claude.com/docs/en/sandboxing&quot;&gt;sandbox&lt;/a&gt; limits what any command can reach, such as which folders it can write to.&lt;/p&gt;
&lt;p&gt;Hooks carry their own risk: &lt;a href=&quot;https://code.claude.com/docs/en/hooks&quot;&gt;Claude Code’s docs&lt;/a&gt; warn that they “execute shell commands with your full user permissions.” Use the strongest layer the risk calls for, and give each agent the fewest permissions it needs; a review agent with read-only tools, as Thoughtworks notes, simply can’t write.&lt;/p&gt;
&lt;h3 id=&quot;step-5-test-the-fix&quot;&gt;Step 5: Test the fix&lt;/h3&gt;
&lt;p&gt;Each of those layers is a hypothesis about this failure. Run the rename task, and a few like it, with and without the new line, the check and the stop, and see which one changed the outcome. Section 7 sets out how.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-context-clean&quot;&gt;Keep the context clean&lt;/h2&gt;
&lt;p&gt;The rename needed one fact, the list of services that read the field, and none of the rest of the repository. That is the general aim. As tokens increase, &lt;a href=&quot;https://www.anthropic.com/research/effective-context-engineering-for-ai-agents&quot;&gt;Anthropic explains&lt;/a&gt;, “the model’s ability to accurately recall information from that context decreases,” so the goal is “the smallest possible set of high-signal tokens.” &lt;a href=&quot;https://platform.claude.com/cookbook/tool-use-context-engineering-context-engineering-tools&quot;&gt;Its cookbook&lt;/a&gt; compares three ways to free up space.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Three ways to free up context: clear old tool output, compact the conversation, or keep notes outside the window.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;753&quot; src=&quot;https://durabilitycurve.com/_astro/fig-context-techniques-anim.DQPOrPyw_Z1Qrsza.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;If you build on the API, start with clearing, which &lt;a href=&quot;https://www.anthropic.com/research/effective-context-engineering-for-ai-agents&quot;&gt;Anthropic calls&lt;/a&gt; “one of the safest lightest touch forms of compaction”; the agent can always fetch the result again. When you compact, tell the summary what to keep. &lt;a href=&quot;https://sourcegraph.com/blog/context-engineering&quot;&gt;Sourcegraph’s&lt;/a&gt; compaction prompt keeps the task, the files already inspected, decisions and why, and “approaches already tried that failed, and why.” Leave that last item out “and the fresh window will cheerfully retry them.”&lt;/p&gt;
&lt;p&gt;Tools take space too. &lt;a href=&quot;https://www.anthropic.com/research/advanced-tool-use&quot;&gt;Anthropic’s tool search&lt;/a&gt; loads tool definitions only when the agent needs them, about 85% fewer tokens in its example, and Claude Code &lt;a href=&quot;https://code.claude.com/docs/en/mcp&quot;&gt;now defers MCP tool definitions&lt;/a&gt; “until Claude needs them.”&lt;/p&gt;
&lt;p&gt;Send noisy research to a subagent and have it return the answer with its evidence and any open questions. Anthropic says a subagent may use “tens of thousands of tokens” and return “often 1,000-2,000 tokens,” and Sourcegraph warns that “a sub-agent that retrieved badly still returns a confident summary.”&lt;/p&gt;
&lt;p&gt;Every connector and skill also puts someone else’s text in the agent’s context. &lt;a href=&quot;https://agentic-engineering.swmansion.com/becoming-productive/harness-engineering/&quot;&gt;Software Mansion&lt;/a&gt; describes a published attack that hid instructions inside a skill in invisible Unicode characters. Read every third-party skill or connector before installing it. That lowers the risk without removing it: hidden text can get past a read, and &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-06-18/server/tools&quot;&gt;a connector’s list of tools can change&lt;/a&gt; after you have read it. How these attacks work, and the guards that limit them, are in &lt;a href=&quot;https://durabilitycurve.com/blog/what-is-prompt-injection/&quot;&gt;What Is Prompt Injection?&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://durabilitycurve.com/blog/remembers-everything-learns-nothing/&quot;&gt;Memory needs curating&lt;/a&gt;. On &lt;a href=&quot;https://www.databricks.com/blog/memcurrent-designing-persistent-memory-production-agents&quot;&gt;Databricks’ internal benchmark&lt;/a&gt; of enterprise tasks, their own memory system scored 22.3% with no memory, 35.8% with searchable raw session traces, and 66.1% with lessons drawn from those traces (68.4% with both). Their caution: “more history is not necessarily better memory.” Give memories an expiry too. Sourcegraph’s example is a memory about a system the team changed weeks ago, which “is worse than no memory: the agent holds a confident, wrong prior it will defend.”&lt;/p&gt;
&lt;h2 id=&quot;work-that-lasts-longer-than-one-session&quot;&gt;Work that lasts longer than one session&lt;/h2&gt;
&lt;p&gt;When a job spans several sessions, &lt;a href=&quot;https://www.anthropic.com/research/effective-harnesses-for-long-running-agents&quot;&gt;Anthropic’s long-running agents experiment&lt;/a&gt; is the most concrete guide. Each new session “begins with no memory of what came before,” and a later agent would “look around, see that progress had been made, and declare the job done.”&lt;/p&gt;
&lt;p&gt;Their fix: a first session writes a &lt;strong&gt;feature list&lt;/strong&gt; of everything “done” means, every item marked failing, in JSON because the model is “less likely to inappropriately change or overwrite JSON files compared to Markdown.” Every later session updates a &lt;strong&gt;progress file&lt;/strong&gt;, ends with a &lt;strong&gt;commit&lt;/strong&gt; so bad changes can be rolled back, and starts by reading both and checking the basics still work.&lt;/p&gt;
&lt;h2 id=&quot;how-to-test-a-harness-change&quot;&gt;How to test a harness change&lt;/h2&gt;
&lt;p&gt;Use this on any change to your harness, including the contract check from section 4. It needs no special tooling, but it does take a fixed set of tasks and a few dozen agent runs. Treat it as a pilot: it will show a large effect, and it cannot prove a small one.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The four-setup test: your original harness, plus the instruction, plus the check, plus both, all graded by the same acceptance tests.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;757&quot; src=&quot;https://durabilitycurve.com/_astro/fig-four-setups-anim.Bh82frDD_Z1zPimW.webp&quot; &gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pick the task class&lt;/strong&gt; your agent failed on, and collect ten or so real examples. Keep a few back that you never look at while building the fix; those held-out tasks show whether the fix generalises or only fits the examples.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grade every setup the same way.&lt;/strong&gt; Decide what “done” means before you start, with held-out acceptance tests the agent never sees, and use them for all four setups. If the contract check were also the grader, setup C would win by definition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run each setup on the same tasks, several times.&lt;/strong&gt; &lt;a href=&quot;https://developers.googleblog.com/the-anatomy-of-harness-engineering-how-to-evaluate-iterate-and-guard-ai-coding-agents/&quot;&gt;Google’s team&lt;/a&gt; recommends running evals in batches, because single runs “can be noisy due to nondeterminism of AI models.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record six numbers:&lt;/strong&gt; tasks passed, tasks verified done by a check you trust, human corrections needed, tokens, time and cost. Count cost honestly; in data from &lt;a href=&quot;https://www.anthropic.com/research/multi-agent-research-system&quot;&gt;Anthropic’s research system&lt;/a&gt;, agents used about 4 times the tokens of chat, and multi-agent systems about 15 times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the cheapest setup that holds up.&lt;/strong&gt; If C does as well as D, the instruction line is not earning its place. If none beats A, the failure may lie elsewhere, or the effect may be too small for a test this size; add tasks before you decide.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That comparison measures outcomes. Google’s team separates outcome benchmarks from behavioural evals: checks of one action at a time, such as whether the agent searched the other services before renaming, which show why a score moved where a big benchmark only shows it moving “by a few percentage points.” Make each check strict for simple tasks, or use an AI judge where several paths are fine. When you change model, run the whole test again; the assumptions in your harness may have gone stale.&lt;/p&gt;
&lt;h2 id=&quot;which-fix-for-which-failure&quot;&gt;Which fix for which failure&lt;/h2&gt;
&lt;p&gt;Most agent mistakes fall into a few kinds, and each has a cheapest fix worth trying first. This table is our synthesis of the guides above; treat each row as a starting hypothesis.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Which fix to try first for each kind of failure, and what to escalate to if it recurs.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;  width=&quot;1456&quot; height=&quot;791&quot; src=&quot;https://durabilitycurve.com/_astro/fig-failure-to-fix-anim.DtU1sKpH_Z1vCm2i.webp&quot; &gt;&lt;/p&gt;
&lt;p&gt;The rename starts in the first and third rows. Once the line is in AGENTS.md, a repeat moves it to the second: the agent knew the rule and broke it, so the next fix is the contract check, and the stop on public API changes is the escalation.&lt;/p&gt;
&lt;h2 id=&quot;the-checklist&quot;&gt;The checklist&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This week&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Your agent’s last mistake has one check that would have caught it, silent when it passes, with the fix in its error.&lt;/p&gt;
&lt;p&gt;☐ Your instructions file holds only what the agent can’t find for itself, plus the rule it last broke.&lt;/p&gt;
&lt;p&gt;☐ Actions you can’t undo (migrations, deletes, pushes) are blocked by a deny rule, a sandbox or a hook.&lt;/p&gt;
&lt;p&gt;☐ The checks that decide “done” are out of the agent’s reach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Your latest fix has been run against the setup without it, on held-out tasks.&lt;/p&gt;
&lt;p&gt;☐ Noisy research goes to a subagent, which returns the answer with its evidence.&lt;/p&gt;
&lt;p&gt;☐ Only the tools and connectors you use are switched on, and every third-party one has been read.&lt;/p&gt;
&lt;p&gt;☐ Long tasks have a done-list and a progress note.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Once it works&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;☐ Each failure you’ve fixed has a small behavioural test.&lt;/p&gt;
&lt;p&gt;☐ On a failed run, you check whether the answer was ever in the context.&lt;/p&gt;
&lt;p&gt;☐ Memories have an expiry or a review date.&lt;/p&gt;
&lt;p&gt;☐ After a model change, every part of the harness is re-tested, and what no longer helps goes.&lt;/p&gt;
&lt;h2 id=&quot;the-whole-job&quot;&gt;The whole job&lt;/h2&gt;
&lt;p&gt;When the agent makes a mistake, find the smallest fix that would have caught it, test it against the setup without it, and keep it only if it earns its place. This week, start with the last mistake your agent made.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Further reading: if you read three of the originals in full, make them &lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;Böckeler on guides and sensors&lt;/a&gt;, &lt;a href=&quot;https://www.anthropic.com/research/effective-harnesses-for-long-running-agents&quot;&gt;Anthropic on agents that work across sessions&lt;/a&gt; and &lt;a href=&quot;https://www.thoughtworks.com/insights/blog/architecture/engineering-the-harness-a-practical-pattern-for-reliable-coding-agents&quot;&gt;Thoughtworks on the renamed field&lt;/a&gt;. Every other source is linked where it is used. They date from June 2025 to October 2026, and every link and quotation was checked on 22 October 2026.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The next guide in this series, &lt;a href=&quot;https://durabilitycurve.com/blog/ai-agent-permissions/&quot;&gt;AI Agent Permissions&lt;/a&gt;, follows one deleted branch through five guards and shows which of them hold.&lt;/p&gt;</content:encoded></item></channel></rss>