Claude Code System Prompt: Engineering Lessons for Agent Builders
Last month I watched an internal coding agent delete the wrong generated file, “fix” the test failure it caused, and then confidently report success. The root bug was not model quality. It was a bad boundary: the agent had permission to edit too much, vague rules about generated artifacts, and no required confirmation before destructive actions.
That is why the recent public discussion around Claude and Claude Code system prompts is useful, even if you should not treat leaked or collected prompts as gospel. Prompt dumps are often unofficial, incomplete, stale, or extracted from a product state that no longer exists. Claude Code itself evolves quickly, and the current Claude lineup — Opus 4.8, Sonnet 4.6, Haiku 4.5, and Fable 5 with 1M context — gives agent builders very different trade-offs than older generations.
But the interesting lesson is not “copy this prompt.” The useful lesson is architectural: mature coding agents encode operational boundaries, instruction priority, confirmation rules, file access discipline, and failure behavior into the system around the model.
This post is about those lessons.
Claude Code System Prompt Lessons Without Copying the Prompt
The phrase “Claude Code system prompt” has become a shortcut for “how does a serious coding agent tell the model to behave?” That is the wrong question if it leads to prompt archaeology, but the right question if it leads to better agent design.
In practice, the system prompt is only one layer. A production-grade agent usually has at least five cooperating control surfaces:
| Layer | What It Controls | Example |
|---|---|---|
| System instructions | Stable behavioral contract | “Prefer minimal changes; do not commit unless asked.” |
| Developer policy | Product-specific rules | “Use apply_patch; never run destructive commands without approval.” |
| Tool schema | What the model can actually do | read_file, edit_file, run_shell, update_plan |
| Runtime permissions | What the process is allowed to access | Sandbox, working directory, network policy |
| User instructions | Task-specific goal | “Fix the failing auth test.” |
The mistake I see in many homegrown agents is collapsing these into one large prompt. That works in demos. It fails under pressure.
A better approach is to use the prompt to explain intent, and the runtime to enforce capability.
For example, do not merely prompt:
Do not delete files unless the user asks.
Make deletion a distinct tool requiring confirmation:
{
"name": "delete_file",
"description": "Delete a file from the workspace.",
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"reason": { "type": "string" },
"user_confirmed": { "type": "boolean" }
},
"required": ["path", "reason", "user_confirmed"]
}
}
Then enforce it outside the model:
def delete_file(path: str, reason: str, user_confirmed: bool):
if not user_confirmed:
raise PermissionError("delete_file requires explicit user confirmation")
if not path.startswith(WORKSPACE_ROOT):
raise PermissionError("cannot delete files outside workspace")
os.remove(path)
The prompt tells the agent how to think. The tool boundary decides what can happen.
Instruction Hierarchy Is an Engineering Interface
Claude models are good at following layered instructions, but you still need to design the layers carefully. If your coding agent has system, developer, project, and user instructions, each layer should have a clear job.
A useful hierarchy looks like this:
- System instructions: identity, safety posture, high-level operating principles.
- Developer instructions: product mechanics, tool usage rules, response style, permission model.
- Project instructions: repo-specific conventions, test commands, architecture notes.
- User instructions: the immediate task.
Claude Code-style workflows make this visible with files like CLAUDE.md, plus command-line configuration and session context. For agent builders, the larger lesson is that project-local instructions should be scoped and override only where appropriate.
A simple project instruction file might look like:
# CLAUDE.md
## Build and Test
- Use `pnpm test -- --runInBand` for focused test runs.
- Do not run `pnpm deploy` unless explicitly requested.
## Code Style
- Prefer small functions over large service classes.
- Do not add new dependencies without explaining why.
## Repo Notes
- API routes live in `apps/api/src/routes`.
- Generated GraphQL types live in `apps/web/src/generated`; do not edit them manually.
A common gotcha: agents often treat all instructions as globally equal. If the user says, “Ignore the project rules and edit generated files directly,” your agent needs to know whether that is allowed. In most setups, user instructions should not silently override higher-priority project or developer constraints.
You can model that explicitly:
PRIORITY = {
"system": 4,
"developer": 3,
"project": 2,
"user": 1,
}
def resolve_conflict(existing_rule, new_rule):
if PRIORITY[new_rule.source] > PRIORITY[existing_rule.source]:
return new_rule
return existing_rule
In a real agent, the model should also explain conflicts to the user:
I can update the source schema and regenerate the types, but I should not edit
`src/generated` directly because the project instructions mark it as generated.
That one sentence prevents a surprising amount of damage.
Tool Boundaries Beat Prompt Politeness
When people inspect coding-agent prompts, they often focus on wording: “be concise,” “act like a senior engineer,” “don’t be sycophantic.” Those are useful, but they are not the core safety mechanism.
The core mechanism is the tool boundary.
A serious coding agent should distinguish between:
- Reading files
- Searching files
- Editing files
- Running commands
- Installing packages
- Accessing network resources
- Deleting or moving files
- Creating commits or branches
- Opening pull requests
- Changing secrets or deployment config
These are not morally equivalent operations.
Here is a practical permission matrix I use when designing agent runtimes:
| Operation | Default Policy | Why |
|---|---|---|
| Read workspace files | Allow | Needed for context gathering |
Search with rg | Allow | Low risk and highly useful |
| Edit tracked source files | Allow with diff visibility | Core coding workflow |
| Run focused tests | Usually allow | Verifies changes |
| Install dependencies | Ask | Alters environment and lockfiles |
| Delete files | Ask | Easy to cause irreversible loss |
| Access network | Ask or deny | Can leak data or create nondeterminism |
| Edit secrets | Deny by default | High blast radius |
| Commit changes | Ask | Repository history is user-owned |
| Deploy | Deny unless explicitly requested | Production impact |
This is also where model choice matters. I like Sonnet-class models for most coding-agent loops because latency, cost, and reasoning balance well. Opus 4.8 is the model I would reach for when the agent needs deeper architectural synthesis or complex debugging. Haiku 4.5 fits fast classification and lightweight repo scanning. Fable 5’s 1M context is interesting for large-codebase comprehension, but long context does not remove the need for good retrieval, scoped reads, and tool discipline.
A million tokens of context can still contain the wrong file.
User Confirmation Should Be Specific, Not Ceremonial
Bad confirmation UX looks like this:
May I proceed?
Proceed with what? Editing one file? Running migrations? Deleting a directory?
Good confirmation is concrete:
I need confirmation before running:
`rm -rf apps/web/.next`
Reason: remove a stale Next.js build cache that may be causing the local
runtime error. This deletes only generated build output and does not touch
source files.
For Claude Code-like agents, this matters because the model may correctly infer the next step but still need human authorization. If every prompt asks for vague approval, users develop approval fatigue and click through. If only high-risk operations require precise confirmation, users stay engaged.
I recommend representing confirmation as structured state, not prose:
{
"action": "run_shell",
"command": "rm -rf apps/web/.next",
"risk": "destructive_generated_artifact",
"requires_confirmation": true,
"confirmation_prompt": "Delete generated Next.js cache at apps/web/.next?"
}
Then your shell runner can enforce it:
def run_shell(command, confirmed=False):
risk = classify_command(command)
if risk.requires_confirmation and not confirmed:
return {
"status": "needs_confirmation",
"message": risk.prompt
}
return subprocess.run(command, shell=True, cwd=WORKSPACE_ROOT)
In practice, command classification will never be perfect. That is fine. Use conservative defaults. If your classifier is unsure whether a command is destructive, ask.
Commands Worth Treating Carefully
These deserve explicit policy:
rm -rf path
git reset --hard
git clean -fd
git push --force
npm install some-package
pip install -r requirements.txt
docker compose down -v
kubectl delete ...
terraform apply
Some are harmless in the right context. Some are catastrophic in the wrong one. The agent should know the difference is contextual and ask when the blast radius is unclear.
File Access Is Context Engineering
The most underrated part of coding-agent design is file access. The prompt can say “inspect relevant files first,” but the runtime should make that behavior easy.
At minimum, give the model tools for:
- Fast filename discovery
- Text search
- Reading bounded file ranges
- Viewing git diffs
- Applying patches atomically
A minimal tool set might look like:
[
{
"name": "search_files",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"glob": { "type": "string" }
},
"required": ["query"]
}
},
{
"name": "read_file",
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"start_line": { "type": "integer" },
"limit": { "type": "integer" }
},
"required": ["path"]
}
},
{
"name": "apply_patch",
"input_schema": {
"type": "object",
"properties": {
"patch": { "type": "string" }
},
"required": ["patch"]
}
}
]
Bounded reads matter. If the agent reads every file in a monorepo, it wastes context and often performs worse. If it reads too little, it guesses. The sweet spot is iterative context acquisition:
rg "createSession|verifySession|SessionStore" apps packages
sed -n '1,220p' apps/api/src/auth/session.ts
sed -n '1,180p' apps/api/src/routes/login.ts
git diff -- apps/api/src/auth/session.ts
Claude Code users will recognize this rhythm: search, inspect, edit, verify. Agent builders should encode that loop into the default behavior.
A common gotcha: file tools need to preserve line endings, permissions, and encoding expectations. I have seen agents “successfully” rewrite shell scripts and remove executable bits, or normalize files in ways that create noisy diffs. Your edit layer should be boring and conservative.
Failure Modes: What Actually Happens When Agents Go Wrong
Most coding agents do not fail dramatically. They fail plausibly.
Here are the failure modes I see most often.
1. The Agent Solves the Symptom
The test fails because an API returns 401. The agent changes the test to expect 401. Green build, broken product.
Mitigation:
- Require the agent to explain the causal chain.
- Ask for focused tests before broad rewrites.
- Prefer source fixes over snapshot updates unless the user requested a behavior change.
2. The Agent Over-Edits
The user asks for a small bug fix. The agent reformats five files, renames variables, and changes public APIs.
Mitigation:
Make the minimal change that fixes the issue. Do not perform opportunistic
refactors unless they are necessary for correctness.
But also enforce diff review. Show changed files and line counts before finalizing:
git diff --stat
git diff -- apps/api/src/auth/session.ts
3. The Agent Trusts Stale Context
Long-running sessions accumulate outdated assumptions. The user changes a file externally, tests now fail differently, and the agent continues from the old mental model.
Mitigation:
- Re-read files before editing if they may have changed.
- Check
git diffbefore applying patches. - Treat tool output as fresher than conversation memory.
4. The Agent Masks Tool Failure
A shell command fails, but the agent proceeds as if it succeeded.
Mitigation: tool results should be structured and hard to ignore.
{
"tool": "run_shell",
"command": "pnpm test auth",
"exit_code": 1,
"stdout": "...",
"stderr": "Cannot find module '@repo/config'",
"status": "failed"
}
Then instruct the agent:
If a tool call fails, do not claim success. Diagnose the failure or clearly
report the blocker.
5. The Agent Writes Secrets Into Context
If the agent can read .env, crash dumps, or production config, it may accidentally include secrets in summaries, logs, or prompts.
Mitigation:
- Block known secret files by default.
- Redact token-like values in tool output.
- Require explicit user approval for secret-bearing paths.
Example path policy:
DENY_PATTERNS = [
".env",
".env.*",
"**/secrets/**",
"**/*.pem",
"**/*.key",
]
def can_read(path):
return not any(fnmatch(path, pattern) for pattern in DENY_PATTERNS)
Prompting the model not to leak secrets is good. Preventing the model from seeing unnecessary secrets is better.
Designing a Claude API Agent Loop
If you are building directly on the Claude API, the simplest useful loop is:
- Send system and developer instructions.
- Provide the user task.
- Let Claude request tools.
- Execute tools in your runtime.
- Return tool results.
- Repeat until the model produces a final answer.
A simplified request shape looks like this:
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"system": "You are a coding agent. Make minimal, correct changes...",
"messages": [
{
"role": "user",
"content": "Fix the auth timeout bug and add a regression test."
}
],
"tools": [
{
"name": "read_file",
"description": "Read a bounded section of a workspace file.",
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"start_line": { "type": "integer" },
"limit": { "type": "integer" }
},
"required": ["path"]
}
}
]
}
A tool-use response conceptually looks like:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "I’ll inspect the auth timeout path first."
},
{
"type": "tool_use",
"name": "read_file",
"id": "toolu_123",
"input": {
"path": "apps/api/src/auth/session.ts",
"start_line": 1,
"limit": 220
}
}
]
}
Your runtime then returns the result:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "export function createSession(...) { ... }"
}
]
}
The exact SDK syntax depends on your client version, but the design principle is stable: Claude proposes actions, your runtime executes or rejects them, and the next model turn receives the outcome.
That separation is what keeps the system understandable.
Claude Code Commands and Settings Worth Studying
Even if you are not using Claude Code directly, its workflow gives useful product patterns.
Typical project bootstrap:
claude
/init
That creates or updates project guidance so future sessions understand repo-specific conventions.
A practical session often looks like:
claude "Find why the billing webhook test is flaky. Do not edit files yet."
claude "Patch the smallest fix and run the focused test."
claude "Show me the diff and summarize remaining risks."
I like this style because it separates investigation, mutation, and verification. Agent builders can mirror that as modes:
{
"mode": "investigate",
"can_edit": false,
"can_run_tests": true,
"requires_confirmation_for": ["network", "delete", "install"]
}
Then switch modes explicitly:
{
"mode": "implement",
"can_edit": true,
"can_run_tests": true,
"requires_confirmation_for": ["network", "delete", "install", "commit"]
}
The important part is not the specific command. It is the interaction contract. Users should know when the agent is only reading, when it is changing files, and when it is about to do something with side effects.
Why Prompt Dumps Are Brittle
The GitHub repository that collected system prompts and model configurations became popular because engineers are naturally curious about how real tools are instructed. I get the appeal. Reading prompts can reveal product taste: what risks the builders worry about, what workflows they optimize, and how they talk to the model.
But copied prompts are brittle for several reasons:
- They may be unofficial. You may not know how they were extracted or whether they are complete.
- They may be outdated. Agent prompts change as models, tools, and product policies change.
- They depend on hidden runtime behavior. A prompt that mentions a tool is useless without the tool implementation.
- They encode product-specific trade-offs. Claude Code’s constraints may not match your agent’s threat model.
- They can create false confidence. A polished prompt does not enforce permissions.
Use these artifacts as design inspiration, not as dependencies.
A better exercise is to ask: what problem is this instruction solving? Then solve that problem in your own stack with both prompt and runtime support.
Practical Takeaways
- Treat the
Claude Code system promptconversation as an architecture discussion, not a copy-paste opportunity. - Put stable behavior in system/developer instructions, repo conventions in project files, and task goals in user messages.
- Enforce high-risk boundaries in tools and runtime permissions, not just prose.
- Require specific confirmation for destructive, networked, dependency-changing, or production-impacting actions.
- Give the agent bounded file access, fast search, atomic patching, and structured tool failures.
- Optimize for the loop that works in practice: inspect, plan, edit minimally, verify, summarize risks.
- Assume copied prompts may be stale or incomplete; build durable agent behavior around explicit capabilities and clear failure handling.
One API key for Claude Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5, plus GPT & Gemini — up to 80% off official pricing, pay-as-you-go.
Get Your API Key →