working-with-legacy-code
Safely change and test untested codebases using Feathers' "Working Effectively with Legacy Code".
Skill by wondelai
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/wondelai-working-with-legacy-code
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: testing
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/working-with-legacy-code
What's inside
- Skill name
working-with-legacy-code- Description
- Safely change and test untested codebases using Feathers' "Working Effectively with Legacy Code". Use when the user mentions "legacy code", "no tests", "untested codebase", "how do I test this", "seams", "characterization tests", "golden master", "sprout method", "afraid to change this code", "monster method", "dependency breaking", or "inherited a messy codebase". Also trigger when changing code without tests safely, getting a class under test when constructors, statics, or singletons block it, adding features to tangled modules, or planning incremental test coverage for an old codebase. Covers the legacy-code change algorithm, seams, characterization tests, sprout/wrap, and dependency-breaking techniques. For refactoring code that already has tests, see refactoring-patterns. For day-to-day code quality, see clean-code.
- License
- MIT
- Metadata
- author: wondelai · version: 1.2.0
- Instructions
- 199 lines in SKILL.md · view on GitHub
Files (6)
- SKILL.md
- case-studies.md · references/
- change-algorithm.md · references/
- characterization-tests.md · references/
- dependency-breaking.md · references/
- seams.md · references/
SKILL.md (first lines)
# Working Effectively with Legacy Code
A field manual for changing code that has no tests, distilled from Michael C. Feathers' *Working Effectively with Legacy Code*. Use it to get untestable classes into a harness, pin down current behavior with characterization tests, and make changes one safe, verifiable step at a time — without resorting to a rewrite.
## Core Principle
**Legacy code is simply code without tests.** Not old code, not ugly code — untested code: without tests you cannot know whether a change preserves behavior, so every edit is a gamble. The craft is breaking dependencies just enough to get tests in place before changing anything — cover and modify, never edit and pray.
## Scoring
**Goal: 10/10.** Rate changes to untested code 0-10 against the principles below. Report the current score and the specific steps needed to reach 10/10.
- **9-10:** Change points covered by characterization tests before any edit; behavior changes and refactoring shipped as separate verified steps; dependencies broken with the least invasive technique
- **7-8:** Tests at most change points, but occasional mixed refactor-plus-behavior commits or heavier dependency surgery than needed
- **5-6:** Some characterization tests, yet key paths still changed on faith; sprouted code accumulating with no payback plan
- **3-4:** Edit-and-pray with manual verification; tests written after the change, asserting whatever the new code happens to do
- **0-2:** Untested edits straight into tangled code, refactoring and behavior change mixed in one commit, rewrite proposed instead of tests
## Framework
### 1. The Legacy Code Dilemma and Change Algorithm
**Core concept:** The dilemma: to change code safely we need tests, but to get tests in place we have to change code. The way out is a fixed sequence — identify change points, find test points, break dependencies, write tests, then make changes and refactor — where the pre-test edits are conservative and mechanical, and the real change happens only inside the safety net.
**Why it works:** Edit-and-pray substitutes care for feedback, and care doesn't scale to code you don't fully understand. Cover-and-modify clamps existing behavior in a vise of tests, so any unintended change announces itself immediately on your machine instead of later in production.
**Key insights:**
- There are two reasons to change code — changing behavior (feature, bug fix) and improving structure (refactoring) — and mixing them in one step makes failures undiagnosable
- Test points are rarely the change points: effects propagate, so you often test where the change's effects surface, not where the edit happens
- Dependency-breaking edits made before tests exist must preserve signatures exactly and lean on the compiler to find every affected site
- Coverage grows along the paths you actually change — that beats any dedicated "testing project" that never gets funded
- "Programming is the art of doing one thing at a time": each step of the algorithm is separately verifiable
**Applications:**
| Context | Application | Example |
|---------|-------------|---------|
| Bug fix in an untested module | Run the five steps before touching the bug | Pin `parseInvoice()` with tests, then fix the rounding error |
| PR mixing cleanup and a feature | Split into structure-only and behavior-only commits | Extract and rename first, tests green, then add the discount rule |
| "It's just a one-line change" | Find the nearest test point first | One pin test at the public method that calls the private one you edit |
See [references/change-algorithm.md](references/change-algorithm.md) when running the five steps on a real change — the algorithm as a working procedure with change-point/test-point checklists and triage for "no time" situations.
### 2. Seams: Where to Pry Code Apart
**Core concept:** A seam is a place where you can alter behavior in your program without editing in that place. Every seam has an enabling point — where you decide which behavior runs. Getting legacy code under test is largely a hunt for seams: spots where a test can substitute a slow, global, or external dependency while the production source stays untouched.
**Why it works:** If you must edit code to test it, you risk changing the very behavior you are trying to pin down. Seams move the substitution to a distance — a subclass, an import, a build flag — so the code under test runs exactly as in production while the test controls its dependencies from the enabling point.
**Key insights:**
- Object seams are the default in OO code: every overridable call is a seam, and its enabling point is wherever the object is created or passed in
- Link and import seams swap implementations at build or load time — `jest.mock` and `unittest.mock.patch` are link seams in modern clothing
- Preprocessing seams (C/C++ macros) are the bluntest instrument; reach for them last
- `new Database()` inside a method body is a seam that never got built — constructors doing real work, globals, statics, and hard-wired I/O are where seams die
- A seam without a reachable enabling point is useless: if the test can't make the decision, keep hunting
- Dynamic languages make nearly every name lookup a seam — cheap, but patching internals couples tests to file layout
**Applications:**
| Context | Application | Example |
|---------|-------------|---------|
| Class constructs its own DB client | Object seam via constructor parameter | `constructor(db: Db = new ProdDb())` — tests pass a fake |
| Module calls a top-level `send_email()` | Import/link seam | `mocker.patch("billing.send_email")` or `jest.mock("./mailer")` |
| Logic reads the wall clock directly | Seam at the clock | Inject a `now()` provider; tests freeze time |
See [references/seams.md](references/seams.md) when hunting a seam in a specific stack — the seam catalog with code, enabling points, and seams in modern tooling (DI containers, jest.mock, pytest monkeypatch, clock and config seams).
### 3. Characterization Tests
**Core concept:** A characterization test documents what the code actually does right now — not what the spec, the comments, or anyone's memory says it should do. Write a probe you know will fail, let the failure message reveal the real behavior, then change the assertion to pin that behavior in place.
**Why it works:** In legacy systems the actual behavior is the de facto spec: callers, reports, and customers may depend on it, quirks included. Tests written from imagined requirements fail for reasons that tell you nothing, while characterization tests fail during refactoring precisely when — and where — you changed existing behavior.
**Key insights:**
- The recipe: call the code in a harness, assert something absurd (`expect(total).toBe(-1)`), read the failure, pin the observed value
- Sensing and separation are the two reasons to break dependencies: separation gets code into a harness, sensing lets assertions see what it computed
- For complex output (reports, generated files, large JSON) use a golden master: capture the full output once, diff against it forever
- Snapshot tests are golden masters — review the first snapshot like code and normalize volatile data, or you are pinning noise
- Found a bug while characterizing? Pin it with a comment and a ticket — downstream code may depend on the wrong behavior; fix it later as a deliberate, separate change
- Characterize the branches your change will touch, not the whole system — coverage follows change
**Applications:**
| Context | Application | Example |
|---------|-------------|---------|
| Refactoring a tax calculator | Pin outputs for representative inputs | Run 20 cases through, assert each recorded result |
| Legacy report generator | Golden master diff | Generate the report, compare to a checked-in master file |
| Off-by-one found while pinning | Pin the wrong value, document it | `assert days == 30 # BUG? expected 31 — TICKET-482` |
See [references/characterization-tests.md](references/characterization-tests.md) when writing your first probe through to a pinned suite — golden masters, snapshot tests done right, and a worked before/after refactor.
### 4. Sprout and Wrap: Changing Without Tests First
**Core concept:** When you genuinely cannot get the area under test today, don't weave new logic into the untested mass. Sprout Method or Sprout Class: write the new behavior as fresh, fully tested code and call it from a single line in the legacy spot. Wrap Method or Wrap Class: rename the old code aside and add behavior before or after the call to it, decorator-style.
**Why it works:** New code in a fresh method or class can be test-driven even when its host can't be instantiated in a harness — testability no longer waits on getting the host into a harness. The untested host changes by exactly one call site, so the unverified blast radius is a single line instead of the whole method.
**Key insights:**
- Sprout Method when new logic plugs in at one point; Sprout Class when the host class won't even instantiate in a test harness
- Wrap Method suits behavior that surrounds the old code (logging, notification, metering) rather than mixes with it: rename `pay()` to `rawPay()`, recreate `pay()` as the wrapper
- Wrap Class is the Decorator pattern — use it when several call sites need the added behavior or the class is already bloated
- Be honest about the trade-off: the host stays untested; you have added good code to a bad neighborhood
- Track sprouts as debt and pay them back — cover the host the next time a change lands there
- Sprouting is a tactical move inside the change algorithm, not a permanent substitute for getting code under test
**Applications:**
| Context | Application | Example |
|---------|-------------|---------|
| Late-fee rule in a 400-line `process()` | Sprout Method, one call line | `total += lateFee(order)` — `lateFee()` written test-first |
| Audit logging around legacy `pay()` | Wrap Method | New `pay()` logs, calls `rawPay()`, logs again |
| New validation, class won't instantiate | Sprout Class | `new OrderValidator().validate(data)` called from legacy code |
### 5. Dependency-Breaking Techniques
**Core concept:** A catalog of mechanical, low-risk moves that sever whatever blocks instantiation or sensing: Extract Interface, Parameterize Constructor, Parameterize Method, Extract and Override Factory Method or Getter, Introduce Instance Delegator, Adapt Parameter, Break Out Method Object, Subclass and Override Method. Because they run before tests exist, always pick the least invasive technique that unblocks you.
**Why it works:** Code resists testing for a small set of recurring reasons — constructors doing real work, statics and singletons, parameters you can't construct, monster methods. Each blocker has a named, practiced counter-move, so you execute a known maneuver instead of improvising surgery on code that has no safety net.From wondelai/skills, MIT.
More from wondelai/skills
- 0.618score
conversion-optimization
wondelai
Guided journey from a leaking conversion flow - landing page, signup, checkout, or in-app onboarding - to a measured, tested funnel.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
mom-test
wondelai
Talk to customers without leading them using Mom Test rules: discuss their life not your idea, ask about specifics in the past, and talk less.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
37signals-way
wondelai
Build lean, opinionated products using the 37signals philosophy from "Getting Real", "Rework", and "Shape Up".
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
blue-ocean-strategy
wondelai
Create uncontested market space using value innovation instead of competing head-to-head.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
clean-architecture
wondelai
Structure software around the Dependency Rule: source code dependencies point inward from frameworks to use cases to entities.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
clean-code
wondelai
Write readable, maintainable code through disciplined naming, small functions, and clean error handling.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
cold-start-problem
wondelai
Start and scale networked products using Andrew Chen's "The Cold Start Problem" framework for network effects.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
contagious
wondelai
Engineer word-of-mouth and virality using the STEPPS framework (Social Currency, Triggers, Emotion, Public, Practical Value, Stories).
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
continuous-discovery
wondelai
Build a weekly cadence of customer touchpoints using Opportunity Solution Trees, assumption mapping, and interview snapshots.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
create-app
wondelai
Guided journey from a raw app idea to a validated, cleanly architected first version that ships on a sustainable cadence.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
create-business
wondelai
Guided journey from raw idea to a validated, positioned, priced business with a chosen beachhead.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
architecture-optimization
wondelai
Guided journey from a working codebase grown slow and tangled to one measurably fast, cleanly bounded, and readable.
SkillBFreshness: B★ 2.5KClaude Code +9
Signals
- Stars
- 2,503
- Forks
- 250
- Open issues
- 6
- Last commit (this folder)
- Aug 9, 2026 (2 months ago)
- Last release
- Aug 29, 2026 (2 months ago)
From GitHub (fetched Oct 24, 2026).
Details
- Version
- 1.2.0
- License
- MIT
- Repository
- github.com/wondelai/skills
- Website
- skills.wondel.ai
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Same skill elsewhere
Copies of this SKILL.md published by others; search shows only one of them.
- 0.323score
working-with-legacy-code
wondelai
Safely change and test untested codebases using Feathers' "Working Effectively with Legacy Code".
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.323score
working-with-legacy-code
wondelai
Safely change and test untested codebases using Feathers' "Working Effectively with Legacy Code".
SkillBFreshness: B★ 2.5KClaude Code +9
More from wondelai
- 0.618score
37signals-way
wondelai
Build lean, opinionated products using the 37signals philosophy from "Getting Real", "Rework", and "Shape Up".
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
blue-ocean-strategy
wondelai
Create uncontested market space using value innovation instead of competing head-to-head.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
mom-test
wondelai
Talk to customers without leading them using Mom Test rules: discuss their life not your idea, ask about specifics in the past, and talk less.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
architecture-optimization
wondelai
Guided journey from a working codebase grown slow and tangled to one measurably fast, cleanly bounded, and readable.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
conversion-optimization
wondelai
Guided journey from a leaking conversion flow - landing page, signup, checkout, or in-app onboarding - to a measured, tested funnel.
SkillBFreshness: B★ 2.5KClaude Code +9 - 0.618score
clean-architecture
wondelai
Structure software around the Dependency Rule: source code dependencies point inward from frameworks to use cases to entities.
SkillBFreshness: B★ 2.5KClaude Code +9
Cite this page
working-with-legacy-code skill. Agent Mart. https://agentmart.dev/skills/wondelai-working-with-legacy-code (last updated 2026-10-24)