The default Claude Code agent is fine for greenfield work. You ask for a feature, it writes the feature, you move on. Where it falls short is the boring, dangerous middle of a real project: an established codebase with existing API consumers, a database that already has data, and a frontend that other teams import from. In that middle, “implement this feature” is almost always also “do not break the seventeen things that already depend on the shape of this code.”
The default agent will happily change a function signature, rename a prop, drop a database column, or alter a JSON response field if you ask for the higher-level outcome. It is not malicious. It is following the principle of least surprise for someone asking the question fresh. The surprise is on the side of the existing consumers, and they are not in the conversation.
fullstack-engineer is the subagent persona I wrote to put a different default in front of the same tools. Same model, same Edit and Write and Bash, same shell environment. The difference is the system prompt: treat every change as potentially breaking, read before you write, and prefer additive over mutative. This post walks through the persona file, when to invoke it, where it makes a difference, and how to adapt the pattern for your own teams.
Where the default falls short, in one scenario
A real shape this often takes:
“Add a
priorityfield to the order model and show it on the order detail page.”
The default agent reads the user model, adds a priority column to the schema (often non-nullable, no default), updates the API serializer to include it, and adds a chip on the detail page. Code looks clean. Then:
- The migration fails on staging because the existing rows have no priority value and the column is
NOT NULLwithout a default. - The mobile app, which uses the same
/orders/:idendpoint, crashes because its strict deserializer does not knowpriorityand the project’s API conventions do not tolerate unknown fields silently. - A reporting job that does
SELECT * FROM ordersand pipes into a CSV exporter starts emitting an unexpected column, breaking a downstream ETL.
None of these are visible from the file the user pointed at. The default agent did the literal task. The actual task was the literal task plus “do not break the seven other consumers of this model.” That second half is exactly what the fullstack-engineer persona is built to make non-optional.
Meet the persona
| Field | Value |
|---|---|
name | fullstack-engineer |
model | opus (this work benefits from careful reasoning) |
memory | user (preferences travel across projects, not just one repo) |
| Tool whitelist | Inherited (Read, Edit, Write, Bash, Grep, Glob, etc.) |
Invoke when the work touches an established codebase with existing consumers: API responses other systems read, database schemas with live data, components used in multiple pages, function signatures with callers across the repo. The phrase to look for in the user’s request is anything that implies the system is already running and you are extending it: “add field X to the order model and show it in the UI”, “refactor the auth flow to support OAuth”, “fix the user profile page when the API returns 404”, “update the response format for /orders.”
The default agent is correct when you are bootstrapping a new module, prototyping, or working in a sandbox where breakage is cheap. Use the persona when breakage is expensive.
The persona file
The frontmatter and a representative slice of the body. The full file lives in ~/.claude/agents/fullstack-engineer.md.
---
name: fullstack-engineer
description: "Use this agent when the user needs to implement, modify, or
extend fullstack features across the codebase. This includes frontend
components, backend APIs, database changes, and integration work. The
agent prioritizes backward compatibility, safety, and careful implementation.
Examples:
- User: 'Add a new status field to the order model and display it in the UI'
Assistant: 'I will use the fullstack-engineer agent to implement this
across the stack safely.'
- User: 'Refactor the authentication flow to support OAuth'
Assistant: 'Let me use the fullstack-engineer agent to handle this
refactor carefully.'
- User: 'Update the API response format for the /orders endpoint'
Assistant: 'This needs careful handling for backward compatibility. Let
me use the fullstack-engineer agent.'"
model: opus
color: red
memory: user
---
You are a senior fullstack engineer with deep expertise in frontend
frameworks, backend services, APIs, databases, and system architecture.
You are known for your careful, methodical approach to development. You
treat every change as potentially breaking and verify backward
compatibility before proceeding.
## Core Principles
1. Safety First: Never make changes that could break existing functionality
without explicit user confirmation. When in doubt, ask.
2. Backward Compatibility: Every change must preserve existing behavior
unless the user explicitly requests a breaking change. This applies to
APIs, database schemas, component props, function signatures, and data
formats.
3. Read Before Write: Always read and understand existing code, patterns,
and conventions before making changes. Never assume how something works.
4. Incremental Changes: Prefer small, reviewable changes over large
rewrites. Each change should be independently verifiable.
5. No Assumptions: If context is missing or ambiguous, ask clarifying
questions before proceeding.
## Backward Compatibility Checklist
Before making any change, evaluate:
- API changes: Will existing clients break? Add new fields/endpoints rather
than modifying existing ones. Deprecate rather than remove.
- Database changes: Are migrations reversible? Use additive migrations
(add columns, add tables). Never drop columns or tables without explicit
confirmation. Always provide default values for new required fields.
- Component changes: Will existing usages break? New props should have
sensible defaults. Never remove or rename existing props without a
migration path.
- Function signatures: Are existing callers affected? Add optional
parameters at the end. Never change the order or type of existing
parameters.
- Data format changes: Will existing stored data still be readable?
Implement format versioning or dual-read capability.
Notice three things about this frontmatter.
First, the description includes example dialogues. Claude Code’s auto-routing reads these to decide when to invoke the subagent. If your descriptions are vague, auto-routing is vague. If they include concrete user phrasings (the literal words a person types when they need this agent), routing improves immediately.
Second, memory: user is deliberate. Backward-compatibility instincts (what your team considers a breaking change, your preferred deprecation cadence, your migration patterns) are about the human and the team, not about one project. Project-scoped memory would force the agent to relearn these for every repo. User-scoped memory lets it carry the lessons.
Third, the system prompt opens with a personality statement, not a task list. “You are known for your careful, methodical approach” is doing real work: it sets the tone the agent uses when it disagrees with the user. The default agent says “Sure, I will do that.” The persona says “Before I do that, here are three consumers of this model. Which ones should I update, and which should keep working with the old shape?”
A walkthrough where the persona earns its keep
Same prompt as above:
“Add a
priorityfield to the order model and show it on the order detail page.”
Default agent flow (paraphrased):
- Read
models/Order.ts. Addpriority: 'low' | 'normal' | 'high'. - Generate a migration that adds a
prioritycolumn. Default to nullable but no fallback value. - Update
OrderSerializerto includepriorityin the JSON response. - Open
pages/orders/[id].tsxand add a colored chip showing the priority. - Report done.
fullstack-engineer flow:
- Read
models/Order.tsand grep for everything that imports it. Find the API serializer, two reporting jobs, the admin tool, and the mobile client’s shared types package. - Surface a plan to the user, before writing any code:
“Adding
prioritywill affect five consumers: the public API response, two reporting jobs, the admin tool, and the shared types package consumed by mobile. I propose: nullable column with default'normal'for backfill, additive change to the serializer, no rename of any existing field. Shall I proceed?” - After confirmation: write the migration with
DEFAULT 'normal'so existing rows get a sensible value and the column is safe to deploy ahead of code that writes to it. - Add
priorityto the serializer as an optional field with a documented default. Note in the PR description: “Mobile client should pin to the new types package version before this lands in production.” - Update the order detail page. Run the existing tests, do not introduce new debug logs.
- Final summary lists: files changed, migrations added, backward-compat surface (what works the same, what is new), and any flags the user should double-check before merging.
The work the persona did that the default did not: the survey of consumers before writing, the explicit migration safety choice, the PR-description note for the mobile team. The actual edit volume is similar. The reasoning before the edit is the differentiator.
When to use the persona vs not
Use it when:
- The codebase is more than a few months old and has multiple consumers per module.
- The request touches schemas, APIs, or component props that other code depends on.
- A breaking change is technically allowed but you want to surface it first instead of merging it silently.
- You want the agent to ask before doing, not after.
Do not use it when:
- You are prototyping or bootstrapping a new module and breakage has zero cost.
- The change is genuinely local: one CSS tweak, a typo in a string, a log-level adjustment.
- You are doing an explicit refactor that wants to break old contracts (in which case write a different persona, or just be explicit in your prompt that you are breaking things on purpose).
- Speed matters more than safety and you accept the risk.
The persona is not a free upgrade. It will run slower than the default. It will ask more questions. Both are the point.
How to adapt the pattern
If you want a similar persona of your own, the fields to think about, in order:
-
name: the slash-command and routing key. Keep it short, kebab-case, descriptive of the stance rather than the tools (fullstack-engineerdescribes a posture;read-edit-write-bashwould describe the toolkit and be useless for routing). -
descriptionwith examples: do not be vague. Write three or four example dialogues using the literal phrasing your team uses when they need this work. This is what auto-routing reads. Generic descriptions get generic invocation. -
model:opusfor high-reasoning work where mistakes are expensive.sonnetfor fast iteration.inheritif you want to defer to whatever the parent session is using. For a persona whose value is caution, opus pays for itself. -
memory:userif the persona’s principles are about the human (their team’s conventions, their risk tolerance, their deprecation style).projectif the persona is about one repo’s specific patterns. Backward-compatibility instincts travel; database column conventions for one specific app do not. -
tools(optional): a whitelist. Omit to inherit everything. Whitelist if you want a persona that cannot run, say, destructive shell commands (omitBash) or cannot write at all (Read-only auditor). -
System prompt opening: write the personality first, the rules second. “You are known for X” sets tone before procedure. A persona that opens with a long checklist reads like a Jira ticket and behaves like one.
-
The checklist: pick the three or four real failure modes you have seen on your team and bake them into a checklist the persona runs before writing. Backward-compat. Migrations. Mock data. Debug logs. Whatever your team has paid for before, encode it.
-
The output format: tell the persona how to summarize its work. The example file ends with “explain what you are changing and why, list backward-compat considerations, highlight risks, summarize all changes.” Without that, the agent’s reports drift in style across sessions.
The pattern generalizes well to any role where there is a default temptation (move fast, ship, do not ask) that you want to invert (move careful, survey, ask first). Code reviewer. Production firefighter. Migration writer. Each is a different opening personality plus a different checklist.
Closing
A persona file is the smallest, most portable form of “this is how my team does it” you can give to an agent. It is plain markdown. It has no build step. It travels with your ~/.claude/agents/ directory across machines. The value of fullstack-engineer is not in what tools it uses (the same as the default) or what model it runs on (the same opus). It is in the opening pages of context the model sees before it touches a file: a posture of caution, a checklist of consumers, and an explicit instruction to ask before changing the shape of anything anyone else depends on.
For the underlying mechanics, see Subagents: anatomy, tools whitelist, system prompt, va examples block which covers how the persona file is loaded and how the examples block influences auto-routing. For the orchestration side, when you want to spawn this persona alongside others in a multi-agent flow, see Spawning patterns: foreground, background, sequential, teammate.