Claude Plugins Pattern: A Skill That Reads Files Before Writing Code
A production-tested Claude Code skill pattern that forces file reads before edits, with the YAML frontmatter, trigger language, and tool restrictions that make it reliable.
Claude Plugins Pattern: A Skill That Reads Files Before Writing Code
The most common failure mode for a Claude Code skill that edits code is the same one humans fall into: writing before reading. The model sees a request, drafts an Edit, and the Edit fails because the old_string doesn't match what's actually in the file. Or worse, it succeeds against a stale assumption and silently corrupts logic.
The fix is a skill design pattern I keep reusing: a contract that makes file reads a precondition, encoded in YAML frontmatter and reinforced in the procedure. This post walks through the pattern with a concrete safe-edit skill, then compares it to two adjacent approaches (raw slash commands and MCP tools) so you can pick the right primitive.
The skill file
Skills live in .claude/skills/<name>/SKILL.md (project) or ~/.claude/skills/<name>/SKILL.md (user). The frontmatter is what the harness reads to decide whether the skill matches a user request. Everything below the frontmatter is loaded into the prompt only when the skill fires.
---
name: safe-edit
description: |
Use this skill when the user wants to modify an existing source file
(Python, Rust, TypeScript, MDX, YAML, TOML). Trigger on "edit",
"modify", "change", "rename", "refactor", "fix this", "update this
file". DO NOT use for creating new files (use `new-file`) or for
multi-file refactors (use `large-refactor`).
allowed-tools: Read, Edit, Grep, Glob
---
The description is doing real work. The Claude harness uses it to decide whether the skill is relevant to the current message. Two rules I've learned the hard way:
- Lead with the trigger phrase, not the implementation. "Use this skill when the user wants to modify..." beats "A skill that wraps Edit with a Read precondition." The matcher cares about intent, not internals.
- Include explicit negative scope. "DO NOT use for creating new files" prevents the skill from being yanked into requests it can't satisfy. Without it, the model will try and the user gets a confusing "I can't do that" mid-task.
allowed-tools is the second half of the contract. By restricting the skill to Read, Edit, Grep, and Glob, you remove the option to call Write (which overwrites files) or Bash (which can run anything). The skill physically cannot misbehave outside that set.
The procedure body
Below the frontmatter, write the procedure as numbered steps. Imperative voice, one decision per step:
## Procedure
1. Read the target file with `Read`. If the user gave a symbol name
instead of a path, run `Grep` first to locate it.
2. Confirm the exact lines you intend to change. Quote them back to
the user if the edit is non-trivial (>5 lines or touches a
function signature).
3. Construct the `old_string` for `Edit` by copying the EXACT bytes
from step 1 \u2014 do not paraphrase or reformat indentation.
4. Apply the `Edit`. If it fails with "old_string not found", re-read
the file (it may have changed) and retry. Do NOT use `replace_all`
as a fallback \u2014 it hides the real mismatch.
5. Report what changed in one sentence. Do not summarize the file.
Step 4 is the load-bearing one. The default failure mode for an LLM is to flail when an Edit rejects: try variants, escalate to Write, or claim success without verifying. Pinning the recovery path to "re-read, then retry" with an explicit "do NOT" closes that escape hatch.
Why this beats a slash command
A slash command (.claude/commands/safe-edit.md) is a similar primitive but with weaker guarantees. Commands are user-invoked \u2014 the user types /safe-edit and the prompt body fires. Skills are model-invoked \u2014 the harness picks them based on the description.
For a workflow like "edit a file safely," skill semantics win for three reasons:
- Coverage: a skill fires on any phrasing that matches the description. A command fires only when the user types its name. Most users won't remember command names; they will phrase requests naturally.
- Composition: skills can disable tools that would let the model bypass the contract. Commands inherit the full toolset of the parent agent.
- Discovery: the harness shows skills in the system prompt only when relevant, keeping the context window tighter than always-loaded command lists.
A command still wins when the workflow needs explicit user kickoff \u2014 /deploy-staging should never auto-trigger from "deploy this."
Why this beats an MCP tool
MCP tools (Model Context Protocol servers exposing tool definitions over stdio) are the right primitive when you need code execution outside the harness \u2014 talking to a database, calling a vendor API, running a sandboxed interpreter. They are the wrong primitive when the workflow is just disciplined use of existing harness tools.
A safe-edit MCP tool would have to reimplement Read + Edit + Grep wrappers, ship its own server binary, and be installed per-machine. The skill is one Markdown file checked into the repo. For pure prompt-engineering contracts, skills are 10x cheaper to author and maintain.
Rough rule of thumb:
- New behavior the harness can't do? \u2192 MCP tool.
- Discipline around existing harness behavior? \u2192 Skill.
- User-explicit ritual or one-shot operation? \u2192 Slash command.
Testing the skill
There's no official test harness for skills, but you can get 80% coverage with two checks. First, eyeball the trigger:
# In a Claude Code session:
> "Edit the timeout in config.toml from 30 to 60"
# The harness should auto-invoke `safe-edit` based on the description.
# If it doesn't, the description is too vague \u2014 sharpen the trigger phrases.
Second, force a failure case:
> "Edit the function `parse_config` to add logging"
# The skill should Read or Grep first, then Edit. Watch for it skipping
# straight to Edit with a guessed old_string \u2014 that's the failure the
# pattern exists to prevent.
If you see the skill skipping the read, the description is firing correctly but the procedure body isn't directive enough. Tighten step 1 with stronger language ("MUST", "before any Edit").
Frontmatter knobs worth knowing
Beyond name, description, and allowed-tools, two fields show up in production skills:
argument-hint\u2014 a one-line hint the harness shows when the skill is invoked, useful for skills that take inputs ("expects: file path + symbol name").model\u2014 pin the skill to a specific model (e.g.claude-haiku-4-5for cheap routing). Most skills should leave this unset and inherit the parent.
There's no version field and no schema validation, so typos in frontmatter fail silently at match time. Write a scripts/lint-skills.py that opens every SKILL.md, parses the YAML, and asserts the keys you expect \u2014 it catches 90% of authoring bugs before they hit the harness.
When to reach for this pattern
Any time you find Claude Code making a class of mistake repeatedly \u2014 editing without reading, running tests before installing dependencies, committing without staging \u2014 encode the precondition as a skill. The 50-line skill file outperforms a 500-word system prompt nudge because it ships with tool restrictions and a focused trigger.
The pattern generalizes: safe-rename (Grep all references first), migration-only (allowed-tools restricted to Bash for alembic only), read-only-review (drop Edit and Write entirely). Each one is the same shape \u2014 narrow trigger, narrow tools, numbered procedure, explicit don'ts.
References: