ReadingCode Over Context: Why Written Agent Skills Break in Production
7 min read

Code Over Context: Why Written Agent Skills Break in Production

The rule rides along as 12,822 tokens of prose every turn, and nothing enforces it.

One rule from my repo's AGENTS.md, kept two ways. As prose, the agent pays 12,822 tokens for the rules on every turn, and a template with a missing import still reaches the server and fails with a 500. As a schema and a gate script, the rules cost 180 tokens, and the script stops the same file with exit 1.Two columns show input tokens every turn and a broken file. A cross marks Prose in AGENTS.md: a long bar reads 12,822, and the file's arrow crosses a dashed line and slips through. A check marks Schema + gate script: a thin bar reading 180 sits in a dashed outline as long as the first bar, and the file stops at a solid wall, blocked, exit 1. input tokens every turn broken file Prose in AGENTS.md 12,822 slips through Schema + gate script 180 blocked exit 1 One rule from my repo's AGENTS.md, kept two ways. As prose, the agent pays 12,822 tokens for the rules on every turn, and a template with a missing import still reaches the server and fails with a 500. As a schema and a gate script, the rules cost 180 tokens, and the script stops the same file with exit 1.Two columns show input tokens every turn and a broken file. A cross marks Prose in AGENTS.md: a long bar reads 12,822, and the file's arrow crosses a dashed line and slips through. A check marks Schema + gate script: a thin bar reading 180 sits in a dashed outline as long as the first bar, and the file stops at a solid wall, blocked, exit 1. input tokens every turn broken file Prose in AGENTS.md 12,822 slips through Schema + gate script 180 blocked exit 1

One rule among 34, read every turn

Step 1: AGENTS.md holds 34 rules in plain English. One says: import every component you render. The agent reads all 34 rules on every turn. Nothing has been checked yet. AGENTS.md 34 rules in plain English Import every component you render agent reads all 34 rules on every turn

The prose costs 12,822 tokens every turn

Step 2: four equal bars for turn 1, turn 2, turn 3 and turn 20. Each bar is 12,822 tokens, because the model reads the whole file again on every turn. tokens the model reads turn 1 12,822 turn 2 12,822 turn 3 12,822 turn 20 12,822 …

A broken template slips past the prose

Step 3: broken.astro renders ProofVideo but never imports it. The text checks pass. The server render fails with HTTP 500. broken.astro import Layout <ProofVideo /> no import text checks pass server render HTTP 500

The gate costs 180 tokens and says no

Step 4: the schema costs 180 tokens, a thin bar next to a dashed outline of the 12,822 tokens of prose. The gate script blocks the broken file with exit 1. tokens the model reads prose rules 12,822 schema + gate 180 broken file blocked exit 1
1 of 4
Figure 1. One rule from my repo's AGENTS.md, kept two ways. As prose, the agent pays 12,822 tokens for the rules on every turn, and a template with a missing import still reaches the server and fails with a 500. As a schema and a gate script, the rules cost 180 tokens, and the script stops the same file with exit 1.

I distill written skills into deterministic code tools. The frontier model explores a task once, and from then on fast serverless and on-device runtimes run the code instead of rereading the instructions.

Listen to the audio overview(3:04)Fenrir Studio Voice
0:00
3:04

My coding Agent A program that calls a language model in a loop and lets it use tools, such as search or code. reads a rule file on every turn. In this site's own repo, that file is AGENTS.md. It held 34 rules in plain English, and my agent loaded all of them every time. I measured the file with the Gemma Tokenizer The program that cuts text into the tokens a model reads. Each model family has its own.: 12,822 Token A small piece of text, often part of a word. Models read, write and bill text by the token. a turn. The Schema A formal description of the shape data must have, such as which fields exist and their types. that enforces the same rules in code is 180 tokens. Then I dropped a component into a template without importing it. Every text check passed. The Server render The server builds a page's HTML before it sends it. A crash there sends the reader an error page. failed with a 500. The script behind the schema caught the missing import and Exit code The number a program returns when it ends. 0 means success; any other number, such as 1, means failure..

That was one file. When I loaded 10 Markdown A plain text format with light marks, such as # for a heading. Agents often keep notes in it. skill files into the System prompt Fixed instructions at the start of every request. They tell the model its role and its rules., my Harness The code around an agent that hands it the task, runs its tools and checks its work. spent 22,000 input tokens a turn before I typed anything. At Gemini 3.1 Pro input pricing, that is $0.044 per turn in skill tokens alone. On smaller models, instruction-following collapsed at turn 4.

My core thesis: Stop writing prompts for what code can guarantee. A Frontier model The largest, most capable and most expensive tier of models. I keep it for the hardest reasoning. explores a task once. I Distill To turn a long, careful process into a short program that does the same job. what it did into a typed tool. From then on, fast Serverless A cloud service runs my code only when requests come, and adds or removes machines for me. and On-device Runs on the phone, laptop or browser itself, with no call to a server. models call the tool instead of rereading the instructions.
PART 01

The context trap and fragile models

01

Prompts that grow every turn

As my agent took on more jobs, I wrote more markdown files of instructions. They described CLI Command line interface. A program I run by typing its name and options in a terminal. flags, formatting rules and how to recover from errors. My early agent harness put all of these files into the system prompt on every turn.

That habit caused three severe problems in my production workloads:

  • A token tax that compounds: Every turn paid for the files again. Over a 20-turn session, 22,000 tokens of instructions became 440,000 input tokens. My API bill grew in a straight line with the length of the conversation.
  • Attention that fades: Once the Context window All the text a model reads for one answer: its instructions, the chat so far, and any documents. filled past 70% of KV cache The attention keys and values a model saved for tokens it already read. Reusing them skips recomputing that prefix. capacity, my models suffered severe Attention The step where a model compares each token with the tokens before it to decide what matters. decay. They missed critical rules buried in the middle paragraphs.
  • Drift from turn to turn: Instructions in plain English are suggestions. The model follows them most of the time, not every time. My models sometimes skipped validation steps, invented CLI parameters that do not exist, or formatted their output in different ways.

Recent 2026 tool-attention research confirms what I measured in production. Eager prompt skill injection puts every skill into the prompt up front. It costs 10,000 to 60,000 tokens per turn, and it breaks multi-step reasoning once KV cache use crosses 70%. Lazy tool schema loading waits until a tool is needed, and it cuts token overhead by 95.0%.

02

Why written skills fail on light models

Written skills quietly tied my stack to massive frontier models. Frontier reasoning models had enough capacity to follow my multi-step instructions, even when the prompt was unclear. Smaller runtimes failed at once.

When I tried to run my markdown-based agent on lighter runtimes, it broke in two different ways:

  • On-device runtimes: Models that run on a phone or in a browser have small context windows, often 2K to 8K tokens, and a strict local compute budget. They could not take in ten pages of markdown rules and still keep track of the conversation.
  • Fast serverless endpoints: Light cloud models have large context windows. But when I filled them with markdown skills, attention degraded at turn 4, each turn got slower, and my token costs multiplied across multi-turn sessions.

Parsing a timestamp or checking a JSON A common text format for structured data. A parser needs the whole object; half of one fails. payload is simple work. If my agent needs a frontier model to do it, my system is fragile, both in cost and in design.

PART 02

The brain and the exoskeleton

03

The exoskeleton vs. the brain

I want agents that work on every model tier. So I split each agent into two layers. One layer reasons. The other layer executes, and it is Deterministic Always gives the same output for the same input. Ordinary code works this way; a model does not.: the same input always gives the same result. I call this split my Elastic Cognitive Envelope:

  • The brain (probabilistic reasoning): Working out what the user wants, breaking a vague goal into steps, creative synthesis and high-level strategy. I keep this layer inside the model.
  • The exoskeleton (deterministic code): State machine Code that tracks which step a task is on and allows only the moves defined from that step., schema validation, arithmetic, API authentication and changes to files. I enforce this layer in compiled or interpreted code.

The code itself adds nothing to the prompt; only its schema does. It runs in milliseconds, and ordinary Unit test A small automatic test that runs one function with a known input and checks the output. can cover it. When I move the rules that must always hold into code, my system prompt shrinks from thousands of lines to a 180-token tool schema.

Where the exoskeleton runs depends on security, compute and platform limits. I use three setups:

  • In-process execution: In my local developer tools and CLI agents, the exoskeleton runs inside the host process, like my git engine below. It runs system commands with zero network overhead.
  • Client-side runtimes: In my mobile and web apps, I call Gemini models through client SDK Software development kit. A library a vendor ships so my app can call its service. such as Firebase AI Logic. My app can then run local tools on the device, in its own process.
  • Stateless serverless services: Some tools need private credentials, heavy dependencies or privileged infrastructure. I package those as stateless containers on Google Cloud Run. When client apps call these endpoints, I guard them with Firebase App Check. It uses cryptographic Attestation Proof, signed by the device or platform, that a request comes from my real app and not from a script. to block calls that do not come from my app.
04

Three stages from prose to code

To move from written instructions to deterministic code, I run a three-stage distillation workflow:

  1. Stage 1 (frontier exploration): A frontier model with extended thinking explores a problem that is still unclear. It calls the APIs and finds the edge cases.
  2. Stage 2 (code distillation): Once the workflow is stable, I ask the frontier model to turn the multi-turn session into a typed script with strict input and output schemas.
  3. Stage 3 (light runtime deployment): I expose the script as a single tool call. Fast serverless and on-device models call it with 180 tokens of schema overhead and zero execution drift.
PART 03

Implementation and benchmarks

Here is the gate from the lead, step by step. It is a short Node script, verify_import_coverage.mjs, and it checks one rule from AGENTS.md: a template must import every component it renders. Figure 2 runs it on the same broken template. The video after it records the agent's run in my editor, from the token count to the exit 1.

The gate lists imports and rendered tags

Step 1: the gate reads broken.astro. The imports at the top name Layout. The template renders two tags: Layout and ProofVideo. imports at the top Layout tags it renders vs Layout ProofVideo

ProofVideo has no import, so exit 1

Step 2: ProofVideo is rendered but never imported. The script exits 1 and names the tag, before the build can turn it into a 500. imports at the top Layout tags it renders vs Layout ProofVideo no import exit 1 stopped before the build

Add the import, and the check passes

Step 3: with ProofVideo imported at the top, the script reports 2 component tags, all bound, and exits 0. imports at the top Layout ProofVideo tags it renders vs Layout ProofVideo exit 0 2 component tags, all bound
1 of 3
Figure 2. The gate on broken.astro, the template from the lead and the video. It compares the imports at the top of the file with the component tags the template renders. A tag with no import fails with exit 1 before any build. With the import added, the same script exits 0.
code-over-contextScreen recording
Agent turn in Antigravity: working through a three-step list, the agent measures 12,822 tokens/turn for AGENTS.md vs 180 for the schema, then the import coverage gate exits 1
Show the shell command (reproduce locally)
node verify_import_coverage.mjs broken.astro
05

A distilled tool in TypeScript

Here is one distilled tool. Instead of reading a 500-line markdown guide on git branching and commit hygiene, my agent calls this typed function. The function enforces the rules the same way every time:

distilledToolHarness.ts
TypeScript
import { z } from 'zod';
import { execFileSync, spawnSync } from 'node:child_process';
import { statSync } from 'node:fs';
import { join } from 'node:path';

// 1. Strict input schema replaces 40 lines of prompt formatting rules
export const CommitActionSchema = z.object({
  branch: z.string().regex(/^[A-Za-z0-9._/-]+$/, 'Invalid branch name format').refine(b => !b.startsWith('-'), 'Branch name cannot start with a dash'),
  message: z.string().min(10).refine(m => m.split('\n')[0].length <= 72, 'Subject line must be 72 characters or fewer'),
  base: z.string().regex(/^[A-Za-z0-9._/-]+$/).refine(b => !b.startsWith('-')).optional(),
  files: z.array(z.string().refine(f => f.split('/').every(s => s !== '' && s !== '.' && s !== '..'), 'Must be a file path relative to the repository')).nonempty(),
  signoff: z.boolean().default(true),
});

// z.input keeps signoff optional for callers; parse() fills in the default
export type CommitAction = z.input<typeof CommitActionSchema>;

// Every git call gets a fixed repo, a timeout, captured stderr, and no pathspec magic
const git = (repo: string, args: string[]) =>
  execFileSync('git', ['--literal-pathspecs', ...args], { cwd: repo, encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'] });

// 2. Deterministic execution engine replaces multi-turn prompt retries
export class DistilledGitEngine {
  public static execute(action: CommitAction, repo: string): { success: boolean; hash?: string; committed?: boolean; error?: string } {
    try {
      // Validate schema contracts before touching disk
      const { branch, base, message, files, signoff } = CommitActionSchema.parse(action);
      for (const f of files) if (statSync(join(repo, f), { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${f} is a directory; name each file`);

      // switch never reads the name as a path; create the branch only if it does not exist
      const exists = spawnSync('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: repo }).status === 0;
      git(repo, exists ? ['switch', branch] : ['switch', '-c', branch, ...(base ? [base] : [])]);

      // '--' blocks option injection; --literal-pathspecs blocks '.' and ':/' magic
      git(repo, ['add', '--', ...files]);

      // Commit only the named files, never whatever else was staged; a retry with nothing new is a no-op
      const committed = git(repo, ['diff', '--cached', '--name-only', '--', ...files]) !== '';
      if (committed) git(repo, ['commit', '-m', message, ...(signoff ? ['--signoff'] : []), '--', ...files]);

      // Retrieve commit hash deterministically rather than parsing stdout
      const hash = git(repo, ['rev-parse', 'HEAD']).trim();
      return { success: true, hash, committed };
    } catch (err) {
      // Return structured, actionable error instead of raw stack trace
      return { success: false, error: err instanceof Error ? err.message : String(err) };
    }
  }
}

The schema rejects a bad branch name, a long subject line or a path outside the repo before git runs at all. The -- before each file list stops a file name from being read as a flag. The function returns a plain result, so the agent gets a clear error instead of a stack trace.

06

Written skill or code: the trade-offs

When I decide whether a job belongs in a written skill or in a distilled code tool, I compare five things:

DimensionPrompt-Heavy Written SkillDistilled Code-First ToolHybrid Distillation Pattern
Context overhead22,000 tokens per turn (10 skill files)180 tokens (schema only)180 tokens (schema only)
Execution latency1,100 to 2,800 ms per turnAbout 85 ms (five git calls)About 85 ms (deterministic code)
Model tier supportFrontier reasoning models onlyAll tiers (On-Device, Workhorse, Frontier)Frontier for authoring, Workhorse for runtime
ReliabilityProbabilistic (collapses at turn 4 on light models)DeterministicDeterministic, verified by unit tests
Authoring velocityFast initial draftRequires manual engineeringFast (frontier model synthesizes code)

Industry validation and benchmarks

First published 25 Aug 2026 · last revised 9 Oct 2026 · 29 revisions

Hand this to your agent

You

Read this essay by Ibrahim Ulukaya, https://ulukaya.dev/posts/code-over-context, and look at my project. Which written skills or markdown skill files does my agent reread per turn that I could distill into deterministic code tools? Name the file and line for each finding.

Cited by
  1. The Crutch vs. the Operating System: Why I Deleted 4,000 Lines of Agent Prompts

    ……rules from my coding agent harness. For two years those natural language instructions were my main defense. They were meant to stop invented……