Overreach

Asked for a UI tweak, it changed backend logic

只让改个前端样式,它动了后端逻辑

A small frontend request ("make the button say Save", "hide this field") comes back with changes to API handlers, validation or database queries.

Applies to: Claude Code · Cursor · Codex · Copilot agent

Symptom

The UI change works and nothing is buggy, but the diff touches `api/`, `server/` or `db/`. A field got removed from the API response instead of hidden in the view, or validation was "aligned" with the new label.

Why it happens

The agent picks the change that most directly makes the visible result true, wherever it lives. Without a declared layer boundary, "hide the field" can be satisfied in the API as easily as in the component. It is the example @liushiyumath gives for part 3 of the three-part view (works, bug-free, not how you wanted it). Lawrence Jones described the same kind of thing as agents that "sometimes do things that aren't ideal".

Example

Illustrative.

text
Request: "Don't show the phone number on the profile card."

M src/components/ProfileCard.tsx   - <p>{user.phone}</p>
M server/api/users.ts              - phone: user.phone,   // removed from the API for every client

Copy-paste fixes

Pick the files for the agents you use. Snippets are in English so they work in any repo.

Rules

Claude Code: add to CLAUDE.md

CLAUDE.md
## Respect layer boundaries
- Frontend requests change frontend code only (components, styles, copy, client state).
  Do not edit `server/`, `api/`, `db/`, migrations or shared validation for a UI request.
- If the UI change seems to need a backend change, stop and propose it separately: what, why,
  and which other clients would be affected.
- The reverse holds too: backend tasks don't restyle the UI.
Rules

Codex, Copilot coding agent and others: add to AGENTS.md

AGENTS.md
## Respect layer boundaries
- Frontend requests change frontend code only (components, styles, copy, client state).
  Do not edit `server/`, `api/`, `db/`, migrations or shared validation for a UI request.
- If the UI change seems to need a backend change, stop and propose it separately: what, why,
  and which other clients would be affected.
- The reverse holds too: backend tasks don't restyle the UI.
Cursor rule

Cursor: project rule

.cursor/rules/frontend-tweak-changed-backend.mdc
---
description: "AgentFails: Asked for a UI tweak, it changed backend logic"
alwaysApply: true
---

## Respect layer boundaries
- Frontend requests change frontend code only (components, styles, copy, client state).
  Do not edit `server/`, `api/`, `db/`, migrations or shared validation for a UI request.
- If the UI change seems to need a backend change, stop and propose it separately: what, why,
  and which other clients would be affected.
- The reverse holds too: backend tasks don't restyle the UI.
Hook

Per-task scope file for a frontend-only task

Used by the scope-guard hook below. Write it at the start of the task; delete it when done.

.agent-scope
# Frontend-only task: UI tweak on the profile card
src/components/**
src/styles/**
src/app/**/page.tsx
tests/components/**
Hook

Claude Code PreToolUse hook: block edits outside the declared scope

Adapted from the diff-budget design on dev.to (yureki_lab). Put allowed globs, one per line, in .agent-scope at the repo root. Exit code 2 blocks the tool call and feeds stderr back to the agent. It only covers file-edit tools; shell writes still need the diff check.

.claude/hooks/scope_guard.py
#!/usr/bin/env python3
"""PreToolUse hook: block Edit/Write/MultiEdit outside the globs in .agent-scope."""
import fnmatch, json, os, sys

call = json.load(sys.stdin)
if call.get("tool_name") not in ("Edit", "Write", "MultiEdit"):
    sys.exit(0)

root = os.environ.get("CLAUDE_PROJECT_DIR") or call.get("cwd") or os.getcwd()
scope_file = os.path.join(root, ".agent-scope")
if not os.path.exists(scope_file):
    sys.exit(0)  # no scope declared for this task -> allow

allowed = [l.strip() for l in open(scope_file) if l.strip() and not l.startswith("#")]
target = os.path.relpath(call["tool_input"]["file_path"], root)

if any(fnmatch.fnmatch(target, pat) for pat in allowed):
    sys.exit(0)

print(
    f"BLOCKED: {target} is outside this task's scope ({', '.join(allowed)}).\n"
    "If this edit is required, stop and ask the user to widen .agent-scope, with a one-line reason.\n"
    "If it is just an improvement you noticed, add it to FOLLOWUPS.md and keep going.",
    file=sys.stderr,
)
sys.exit(2)
Settings

Register the scope guard in .claude/settings.json

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scope_guard.py" }
        ]
      }
    ]
  }
}
Skill

Shared /verify skill for Claude Code

Commit it to the repo so the whole team (and every agent session) runs the same checks. Replace the commands with your project's real ones.

.claude/skills/verify/SKILL.md
---
name: verify
description: Verify the current change before calling it done. Use before every commit, PR, or "done" message.
---

# Verify

Run every step and paste the real output. Never summarize a step you did not run.

1. **Scope**: run `git diff --stat` and `git status --short`. For every changed file, say which part
   of the request needs it. Any file you cannot justify gets reverted, or listed under
   "Unrequested changes" for the user to decide.
2. **Build and types**: `npm run build` (or the project's build command).
3. **Tests**: `npm test`. Report pass/fail counts. If a test was edited, skipped or deleted,
   say so explicitly with the reason.
4. **Behavior**: exercise the changed behavior once for real (a curl, a CLI run, a screenshot)
   and show what happened.
5. **Report** in this format:
   - Requested: <one line>
   - Changed files: <list, each with its reason>
   - Checks run: <command -> result>
   - Not done / unsure: <list, or "none">

Sources and credit

Related patterns

翻车 of the week

One real agent failure and its fix, every week. Free, short, no spam.