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.
textRequest: "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.
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.
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: 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.
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/**
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)
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" }
]
}
]
}
}
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">