Summary
Some functions in my codebase are finished. I have hand-tuned them, stress-tested them, and paid in incidents for every edge case they handle. I do not want a coding agent to “improve” them. The fix is to move enforcement out of the prompt and into the harness: use Claude Code or Cursor hooks to reject edits that touch a locked region, and back that up with a CI hash check. Prompts are suggestions. Hooks are walls.
The Problem
Coding agents are trained to be helpful, and helpfulness is unbounded. Ask for a bug fix three files away and the agent will quietly “tidy up” your hand-crafted pricing function on its way through. The change looks reasonable in the diff. It passes tests. It is also subtly wrong in a way that will cost you money in six months.
Telling the agent “do not touch this file” in CLAUDE.md or .cursorrules works most of the time. Most of the time is not good enough for logic that is already correct. I need hard enforcement, not a polite request.
The Solution
Three layers, in order of strength:
- Pre-edit hook blocks the tool call before it lands.
- Post-edit hook catches and reverts anything that slipped through.
- CI hash check is the backstop when hooks are bypassed.
Claude Code and Cursor sit on different sides of this because of how their hook systems work. Claude Code fires PreToolUse before the Edit tool runs, so you can block cleanly. Cursor’s file-edit hook is afterFileEdit, which runs after the write, so enforcement is reactive. Both are useful. Both have gaps. Use them together if you run both tools.
Marking Locked Regions
I use sentinel comments because they work in any language and survive refactors better than line numbers. The function itself declares the lock:
// @locked hash=f3a1c2 reason="pricing invariants validated in INC-204"
export function computeListPrice(sku: Sku, ctx: PricingContext): Money {
// ... hand-crafted logic ...
}
// @end-locked
The hash is a SHA of the bytes between the sentinels at the time you locked it. If the bytes change, the hash no longer matches. That is the signal.
For file-level locking, a header is enough:
// @locked-file — do not edit without an explicit unlock commit
Claude Code: PreToolUse Hook
Claude Code’s PreToolUse hook sees the full tool input before the Edit or Write runs. Return a non-zero exit with a message on stderr and the tool call is blocked. The model sees the rejection in its next turn and routes around it.
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/check-locked.sh"
}
]
}
]
}
}
.claude/hooks/check-locked.sh:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0
[ ! -f "$file" ] && exit 0
if grep -q "@locked-file" "$file"; then
echo "Blocked: $file is marked @locked-file. Propose changes in a comment or request an explicit unlock." >&2
exit 2
fi
# Region-level check: verify locked blocks still hash to their declared value
python3 .claude/hooks/verify_locked_regions.py "$file" || exit 2
Exit code 2 is the important bit. Claude Code treats it as a blocking error and surfaces the stderr message to the model, so the agent explains the conflict instead of silently skipping.
The companion Python script walks the file, finds each @locked ... @end-locked pair, hashes the body, and fails if any hash drifted. This is the function-level enforcement that sentinel comments alone do not give you.
Cursor: afterFileEdit Hook
Cursor’s hook fires after the write, so the shape is different. You detect the violation and revert.
.cursor/hooks.json:
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": ".cursor/hooks/guard-locked.sh",
"failClosed": true
}
]
}
}
.cursor/hooks/guard-locked.sh:
#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.file_path')
if grep -q "@locked-file" "$file"; then
git checkout -- "$file"
jq -n --arg msg "Reverted edit to locked file $file" \
'{permission: "deny", userMessage: $msg}'
exit 0
fi
The reactive shape has a sharp edge: if the edit introduces a syntax error on its way to the revert, you can briefly break the file. Always pair this with git checkout -- from a clean working tree, and run the hook with failClosed: true so a crashed script does not fail open.
For pre-edit blocking on Cursor, beforeShellExecution only covers shell commands, not file writes. The closest substitute is a strong rule in .cursorrules plus the reactive revert above. Claude Code is strictly more capable here.
CI Hash Check: The Backstop
Hooks only run inside the agent. A human editing by hand, another tool, or a merged PR from elsewhere will bypass them. So I also check locked hashes in CI:
# .github/workflows/locked.yml step
python3 scripts/verify_locked_regions.py --all
The script re-hashes every locked region in the repo and fails if any drift from the value declared in the sentinel. Unlocking is a deliberate two-step commit: update the logic, then update the hash in a commit message tagged unlock:. This makes intentional changes visible in review and accidental changes impossible to merge.
Failure Modes to Avoid
Locking too much. If half the repo is locked, the agent starts lying about what it did. “I updated the pricing module” becomes “I pretended to update the pricing module and hoped you would not notice.” Lock the five functions you actually trust. Leave the rest editable.
Silent blocks. A hook that rejects without a clear error message teaches the agent to avoid the file without understanding why. Always return an actionable message: what is locked, why, and what the agent should do instead (usually: propose a diff in a comment and stop).
Forgetting the backstop. Hooks are scoped to the tool running them. CI is where enforcement becomes repo-wide. Do not rely on hooks alone.
Key Takeaways
- Prompts are suggestions. Hooks and CI checks are walls. Put finished logic behind walls.
- Claude Code’s
PreToolUseblocks cleanly. Cursor’safterFileEditreverts reactively. Use both if you run both tools. - Sentinel comments plus a content hash give you function-level locking that survives refactors.
- A CI hash check is the backstop that catches anything the agent harness misses.
- Lock sparingly. Over-locking pushes the agent into silent workarounds, which is worse than an edit.
