← Back to list

Keep Claude Skills Lean: Move Deterministic Logic to Python

When skills grow too large, agents start skipping steps. The fix is moving deterministic logic into Python scripts the skill can call.

Saif Ullah · 2026-05-29 14:29 · 0 claps · 5.3 min read
#claude-skills #ai-automation
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents

Keep Claude Skills Lean: Move Deterministic Logic to Python

When skills grow too large, agents start skipping steps. The fix is moving deterministic logic into Python scripts the skill can call.

The Problem With Big Skills

Every skill starts small with a few hundred words, a clear invocation sequence, and focused instructions. Then the task grows — adding more fields, rules, and edge cases. Before long, SKILL.md hits 3,000 words and the agent begins dropping steps it used to handle reliably.

This is not a bug in the model; it is a context window constraint. When a skill triggers, the full SKILL.md body loads into the active context window. A 3,000-word skill adds roughly 4,000 tokens before the agent reads a single word of your actual task. Transformer attention distributes across everything simultaneously—the more tokens competing for focus, the weaker the signal on any individual instruction. The complex, multi-step rules that appear later in the document are usually the first to fail.

Token count is just the symptom. The real issue is that two entirely different types of logic are sitting in the same file, in the same format, when only one of them belongs there.

There is also a direct cost dimension. In a multi-turn agent session, the full context — including the loaded SKILL.md body—is re-sent and billed as input tokens on every single message exchange. A 4,000-token skill running across a 20-turn session adds 80,000 input tokens. Prompt caching can reduce this, but it requires explicit configuration, has a strict 5-minute inactivity TTL, and any change to the cached content invalidates the cache, triggering a full re-write charge. A leaner SKILL.md reduces the baseline cost on every turn, cached or not.

How Context Loading Actually Works

Skills load in three layers:

  1. Startup: Only SKILL.md frontmatter enters context—name and description, around 50–100 tokens per skill.
  2. Trigger: When a skill triggers, the full SKILL.md body loads (Anthropic recommends keeping this under 5,000 tokens).
  3. Execution: When the skill calls a Python script via bash, the script code never enters context. Only stdout does.

(https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)

That third point is your core architectural lever. A 500-line Python script that validates a schema, populates structured fields, and returns a JSON object costs only the tokens in its output. The same logic written as natural language instructions in SKILL.md costs every token it occupies on every single run, even when that logic is irrelevant to the current task.

Where to Draw the Line

The common advice is “use Python for deterministic tasks.” That is correct but not specific enough. Data type is the clearest signal.

We built a skill to extract a structured schema from mixed sources — some structured (database tables, typed document fields), some unstructured (free-text sections, narrative descriptions). Putting database column lookups, validation conditions, and prose instructions all into SKILL.md was an architectural mistake. When a field maps directly to a database column—entities.system_code maps to source_system, no interpretation needed—there is no reason to ask the LLM to execute it. It is a lookup. Python handles it faster, cheaper, and without any risk of the model forgetting it under context pressure.

Move to Python:

  • Schema definition and validation logic: Type checks, required fields, referential integrity, and cross-field constraints.
  • Deterministic extraction rules: Mapping database columns, document fields, or API response keys directly to schema fields.
  • Why: Each rule has a single correct answer, can be unit tested, and has no reason to occupy runtime context.

Keep in SKILL.md: Instructions for extracting fields from unstructured sources where the model needs to read and interpret content. Keep the core execution sequence and fallback instructions here for when structured extraction returns nothing.

The Rule of Thumb: If the logic could have a unit test, it belongs in Python.

What This Looks Like in Practice

We had a single SKILL.md with a 12-field extraction schema. Extraction rules for both structured and unstructured sources were inline, alongside validation conditions written as natural language — around 2,800 words. The agent would silently skip populating complex fields, drop validation steps, or bypass derivation logic entirely. The instructions were there. The context was too crowded to act on all of them.

Refactoring separates the logic into a clean structure:

extraction-skill/
├── SKILL.md              # ~600 words: invocation sequence + unstructured instructions
├── scripts/
│   ├── schema.py         # Pydantic schema definition
│   ├── populate.py       # Structured field extraction
│   └── validate.py       # Post-extraction validation

schema.py defines every field and annotates its source directly in the code:

from pydantic import BaseModel
from typing import Optional

class ExtractionSchema(BaseModel):
    entity_id: str
    entity_name: str
    source_system: str          # from DB: entities.system_code
    contract_value: float       # from DB: contracts.total_value
    risk_summary: Optional[str] # unstructured - LLM handles this
    classification: str         # from doc field: metadata.classification
    # Tracks which fields populate.py could not resolve
    pending_llm_fields: list[str] = []  # e.g., ["risk_summary"]

populate.py runs the structured extraction—database queries, document field lookups, typed API responses. It returns a partially populated schema object with pending_llm_fields listing every field it could not resolve from a structured source. That list is the explicit handoff contract—SKILL.md reads it and knows exactly which fields need model reasoning.

validate.py runs independently as an essential security boundary. Because the model operates on unstructured text, it is vulnerable to prompt injections or malicious payloads hidden in document data. Running validate.py deterministically after the LLM layer ensures type violations, missing fields, and out-of-range values are blocked before hitting downstream systems. The model cannot reason its way around a Python type check.

The invocation sequence in SKILL.md simplifies to four lines:

1. Run scripts/populate.py against structured sources. Pass source paths as arguments.
2. Run scripts/validate.py on the result. Surface any errors.
3. For fields listed in pending_llm_fields, apply the unstructured extraction instructions below.
4. Run scripts/validate.py again on the final schema.

SKILL.md shrinks from 2,800 words to 600, preserving the model's focus entirely for reasoning.

Why This Also Makes Skills Easier to Maintain

The context reduction is immediate, but the maintenance benefit is what compounds.

When extraction rules live in prose, changing a rule means editing a sentence inside a 2,000-word document and validating the change by running full, unpredictable agent sessions. There is no unit test and no precise git diff. If a source system renames a column, you must search through the document for every sentence that references the old name.

When the same rule lives in populate.py, it is a simple, testable, one-line change. The diff is exact, and a regression in structured extraction shows up before the agent runs. At scale, maintaining ten skills in prose means carrying ten untested, unlinted documents; shifting that logic to Python builds a resilient test suite.

Trade-offs to Know Before You Start

The pattern has two real costs.

The first is environmental. It requires Python in the execution environment with the right dependencies. In Claude Code’s default environment this is not a problem — bash access and a standard Python runtime are available. In constrained or sandboxed runtimes, check that script execution via bash works before committing.

The second is design time. The boundary is not always clean. Some fields are partially deterministic — like parsing a messy database currency string into a clean float. Normalizing data is deterministic and belongs in populate.py, but you must explicitly write code to flag the field for the LLM if the raw value is completely missing. Handling that two-stage logic explicitly takes more thought upfront, but it results in a system that executes correctly every single time.

The Path of Least Resistance

A bloated SKILL.md pushes critical design decisions into natural language and forces the model to resolve them at runtime—unpredictably. It is the path of least resistance during development, but it guarantees fragility in production.

Moving deterministic logic to Python forces you to make those structural decisions exactly once, upfront, in a format that can be unit-tested, linted, and code-reviewed.

LLMs are extraordinary at reasoning over ambiguity. That is an unique, expensive, and valuable capability. Do not waste their attention on a job that a simple Python function can do better.


메타데이터
post_id
b9009f53f6db
slug
keep-claude-skills-lean-move-deterministic-logic-to-python-b9009f53f6db
url
https://medium.com/@usaif/keep-claude-skills-lean-move-deterministic-logic-to-python-b9009f53f6db
canonical_url
https://medium.com/@usaif/keep-claude-skills-lean-move-deterministic-logic-to-python-b9009f53f6db
author_url
https://medium.com/@usaif
status
ok
fetched_at
2026-06-09 15:37:30