agent-readable-code/SKILL.md
Principles and a linter for writing code that AI coding agents can read and modify correctly. Use when writing, refactoring, reviewing, or structuring code that will be maintained partly by AI agents (Claude Code, Cursor, Copilot, Aider, Devin). Also use when the user asks about agent-friendly code, AI-friendly architecture, how to structure a repo for AI maintenance, why agents fail on certain codebases, whether SOLID/DRY still apply with AI, or wants to audit an existing codebase for AI-readability. Applies proactively when making naming, file-organization, abstraction, or dependency choices in code the user is likely to maintain with agents — even if they don't explicitly ask for agent-friendly output. Ships with a zero-dependency linter (scripts/lint.py) that flags concrete anti-patterns.
npx skillsauth add mikkelkrogsholm/dev-skills agent-readable-codeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Research-informed principles for writing code that AI coding agents comprehend and modify correctly, plus an advisory linter that flags the most common anti-patterns.
Humans get lost in complexity. Agents get lost in indirection. Classical principles like SOLID and DRY were calibrated for human readers juggling cognitive load; several of them invert when the reader is an agent with a limited context window navigating by grep and file reads alone.
The underlying heuristic: write so that someone grepping a single file can act correctly without reading the rest of the repo.
A note on evidence: some of these practices are well-supported by controlled studies (naming, accurate documentation, verification loops). Others are operational heuristics whose direction is well-motivated but whose thresholds are tunable. The evidence strength for each rule is documented in references/research.md so you can apply judgment rather than follow rules blindly.
For longer before/after examples, see references/patterns.md. For the linter, see scripts/README.md.
Each practice lists the failure mode it prevents, the pattern to apply, and the lint rule that detects violations.
AR003)Agents localize code by grepping names. Names are retrieval cues, not just labels — obfuscating them drops model comprehension accuracy from ~87% to ~59% in controlled studies. A misleading name is worse than a random one, because the agent confidently acts on the wrong mental model.
chargeCustomerAndEmitReceipt beats process.Manager, Service, Helper, Handler, Util, process, handle, doStuff, single-letter vars outside tight loops).utils.py, helpers.ts, misc.py, common.js).The dominant systemic failure across all agent platforms: a feature that spans controller + service + middleware + trigger + background job. The agent fixes three of five touchpoints and ships a subtle bug. Colocate a feature's code, types, and tests in one directory the agent can load as a single coherent unit.
This isn't directly lintable, but shows up as a pattern through AR007 (scattered tests) and AR003 (layer-named files like controllers/, services/ full of generic class names).
AR001, AR002, AR008)str_replace fails when surrounding lines aren't unique. Deduplicate at seams; tolerate duplication inside leaves.dist/ / build/ dirs.AR004, AR005)Code the agent can't trace with grep or a simple AST walk is code the agent hallucinates around. That includes:
__getattr__, eval, exec, importlib.import_module, JS Proxy, Reflect, runtime monkey-patching.registerHandler("user.created", ...) spread across 40 files, or event buses keyed by string. Agents can't trace a string across the repo the way they trace a typed reference. Prefer a discriminated union + one exhaustive switch, or a single registry file imported explicitly by consumers.export * from './foo' files that contain nothing else). They add a grep hop and break tree-shaking. Consumers should import from the defining file.Prefer composition, explicit imports/exports, and direct function calls. Static code graphs are what retrievers (Aider's repo-map, Cursor's embeddings, Claude Code's grep loop) actually traverse.
AR006)AR007)An agent without a feedback loop hallucinates into spirals (documented case: 693 lines of wrong fixes over 39 turns). Every feature should have a way the agent can verify its own change without human help:
foo.ts + foo.test.ts), not siblings in a distant tests/ dir."If you can't verify it, don't ship it" is the highest-leverage rule in every AI coding tool vendor's docs.
The same failure pattern that makes verification loops fragile: non-determinism. Agents cannot debug a test that fails 1-in-10 runs — they retry until context runs out, then either disable the test or declare success.
Math.random, faker.seed(). UUID-in-snapshots is a near-guaranteed flake source.new Date() or time.time() without an injected clock.clock, fetch, logger, env, db as arguments to functions that need them. A function whose signature reveals its side effects is one an agent can test; a function that reaches into globals for process.env is one the agent will quietly call wrong.General principles above apply everywhere. These are idioms per language that have concrete agent-readability wins and that the linter can't fully police.
Python:
__all__ on every module that has public exports. It's the one signal Python has for "import this, not that" — agents otherwise import private helpers and create coupling you didn't intend.slots=True on dataclasses when you don't need dynamic attributes. An agent writing user.foo = bar on a slotted dataclass hits AttributeError immediately; on a normal class it silently succeeds and the bug surfaces three edits later.frozen=True for value objects. Prevents an agent from mutating what it thinks is immutable.TypeScript:
type UserId = string & { readonly __brand: 'UserId' }. Agents routinely swap userId and orgId when both are plain string; branded types make the swap a compile error.type Event = { kind: 'created'; userId: UserId } | { kind: 'deleted'; userId: UserId } — the compiler enumerates cases and agents can't invent a variant.as const on literal tuples and records so types narrow to the literal. Widening to string throws away information the agent needs.z.infer<typeof Schema>, typeof table.$inferSelect. Parallel hand-maintained type declarations drift; an agent updates one, the others silently lie.Agents work best with code that looks like the code they were trained on. This isn't snobbery — it's a real signal:
^ on load-bearing packages). An agent's memory of the API for stripe@v14 is wrong for stripe@v18; pinning makes the docs the agent can fetch match the version you run.Agent-readability is one lens, not the only one. Push back — including on this skill's recommendations — when:
app/ routing, Rails controllers/views/models, Django apps, NestJS modules — framework conventions usually win, even if they scatter a feature across layers. The cost of fighting the framework exceeds the cost of agent-unfriendly structure.process() cannot be renamed without breaking consumers. Evolve it; don't rename it just for AR003.AR007.If a rule fires in a context where it shouldn't, that's a suppression, not a refactor. The linter supports # agent-lint: disable=AR00X inline and # agent-lint: disable-file=AR00X at file scope.
This skill is not "SOLID and DRY are obsolete." Human readers still exist; agents are one reader, not the only reader. Use judgment:
Decide by lifetime and blast radius. A throwaway script has a reader of ~1; skip the ceremony. A payments module maintained for ten years across dozens of contributors (human and agent) earns every clarity investment you make.
The skill ships with scripts/lint.py, a zero-dependency Python linter that flags the rules above.
python scripts/lint.py <path> # human-readable report
python scripts/lint.py <path> --json # machine-readable output
python scripts/lint.py <path> --rules AR001,AR003 # subset
python scripts/lint.py <path> --config my.yaml # custom thresholds
It supports Python and TypeScript/JavaScript across all nine rules. Python uses the stdlib ast module for AR005/AR006; TS/JS uses tight regex heuristics (zero-dep by design — no tree-sitter or typescript dependency). .js/.mjs/.cjs files skip AR006 since they have no type system.
When to run it:
--rules AR001,AR002,AR008 as hard errors and the rest as warnings.See scripts/README.md for the full rule table, configuration, and output format.
Files like CLAUDE.md, AGENTS.md, .cursor/rules/*.mdc, and .continue/rules/ shape how agents approach a repo at a meta level (commands to run, boundaries to respect, canonical examples to imitate). They matter, but they're orthogonal to the code itself being agent-readable. This skill covers the code; follow the vendor docs for the context files.
development
Zod — TypeScript-first schema validation with static type inference. Use when building with Zod or asking about schema definitions, type inference, parsing, transformations, refinements, coercion, error handling, or integration with forms, APIs, or tRPC. Fetch live documentation for up-to-date details.
tools
Vite — next-generation frontend build tool with instant dev server and optimized production builds. Use when building with Vite or asking about its APIs, configuration, plugins, SSR, environment variables, or integration with frameworks. Fetch live documentation for up-to-date details.
tools
Upstash — serverless Redis, QStash, and Vector database with per-request pricing optimized for edge and serverless environments. Use when building with Upstash or asking about its Redis client, QStash message queuing, rate limiting, workflows, or vector search. Fetch live documentation for up-to-date details.
tools
Turso — edge-hosted SQLite database built on libSQL with embedded replicas, multi-tenancy, and low-latency global distribution. Use when building with Turso or asking about its libSQL client, embedded replicas, database-per-tenant patterns, auth tokens, sync, or integration with Drizzle or other ORMs. Fetch live documentation for up-to-date details.