Externalize AI Coding Constraints: Make Rules Executable, Not Spoken
“Please don’t modify the architecture.”
“But you just changed the architecture!”
“I told you three times already!”
Sound familiar? I was stuck in this loop with Claude Code for weeks. Every session started with the same reminders: don’t touch migrations, only modify files in src/auth, run tests before committing. And every session, at least one reminder got ignored.
The problem wasn’t Claude’s memory. The problem was my approach.
The Trap of Verbal Constraints
Verbal constraints—rules we tell the AI during conversation—have fundamental flaws:
- Context overflow: Long conversations bury early instructions
- Token cost: Repeating rules wastes context window
- No enforcement: AI can acknowledge a rule but still violate it
- Inconsistency: Same rule phrased differently each time
I’d remind Claude “don’t modify existing migrations” in the morning. By afternoon, it would suggest editing a migration file. The reminder was lost in the conversation noise.
The Upgrade Rule
After the third time I repeated something to Claude, I realized: if I’ve said it twice, it should become a mechanism.
Verbal reminders are a signal that something should be automated. Not documented—automated. Executed. Enforced.
Where to Externalize Constraints
┌─────────────────────────────────────────────────────────┐│ CONSTRAINT MATERIALIZATION MAP │├─────────────────────────────────────────────────────────┤│ ││ Verbal Rule → Executable Artifact ││ ─────────── ────────────────── ││ ││ "Don't change arch" → ARCHITECTURE.md ││ "Don't touch migrations" → .claude/rules/mig.md ││ "Test after changes" → Hook + CLAUDE.md ││ "Minimal changes only" → Task brief Non-goals ││ "Filter large logs" → Hook preprocessing ││ │└─────────────────────────────────────────────────────────┘Each constraint type has a natural home:
1. File-Based Constraints
CLAUDE.md - Project-level rules loaded at session start:
## Architecture Constraints- Monorepo with packages: web, api, shared- No new packages without discussion- Database changes via migrations/ only
## Done Definition- TypeScript compiles- Tests pass: npm test- Lint clean: npm run lintKeep this under 200 lines. It’s loaded into every session, so density matters.
.claude/rules/*.md - Domain-specific rules:
# Migration Rules- NEVER modify existing migration files- ALWAYS create new migrations for schema changes- Command: npm run db:migrate:create- Test: npm run db:test:migrateThese files are more targeted and can be longer. Claude reads them when relevant.
2. Hook-Based Constraints
Hooks execute before or after tool use. They don’t suggest—they enforce.
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "bash -c 'if [[ \"$FILE_PATH\" == *migrations/*.js && $(git status --porcelain \"$FILE_PATH\" 2>/dev/null | wc -l) -gt 0 ]]; then echo \"BLOCKED: Cannot modify existing migrations\"; exit 1; fi'" } ] } ] }}This hook blocks any edit to existing migration files. Claude can’t override it because it runs before the tool executes.
3. Task Brief Constraints
For specific tasks, use explicit scope boundaries:
## Constraints- Only modify files in src/features/auth/- Do not change User entity schema- Preserve backward compatibility
## Non-goals- NO refactoring unrelated code- NO upgrading dependencies- NO build config changesThe “Non-goals” section is powerful. It defines what success is NOT, which helps prevent scope creep.
Why This Works
┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ Verbal │ │ Written │ │ Executable ││ Constraint │ → │ Document │ → │ Mechanism │└──────────────┘ └──────────────┘ └──────────────┘ ↓ ↓ ↓ Forgettable Referenceable Enforceable Inconsistent Static Dynamic No cost Low cost High valueExecutable constraints are:
- Reliable: They don’t forget or get distracted
- Efficient: Setup once, run forever
- Enforceable: They can block actions, not just warn
Common Mistakes
I made all of these:
Mistake 1: Putting everything in CLAUDE.md
CLAUDE.md becomes a dumping ground. It should contain only the most critical rules that apply to every session. Domain-specific rules belong in .claude/rules/.
Mistake 2: Only using natural language
“Be careful with migrations” is a suggestion. A hook that blocks migration edits is a constraint. The former is easy to ignore; the latter is impossible.
Mistake 3: Not escalating repeated reminders
If I find myself typing the same instruction in multiple sessions, that’s a signal. I should stop and create a mechanism instead.
Mistake 4: Scattering constraints
Without a central mental model, constraints end up in random places. The table above should guide where each constraint type belongs.
The Transformation
Before:
Session 1: "Don't modify migrations" → Claude edits migration → I catch itSession 2: "Don't modify migrations" → Claude suggests migration edit → I reject itSession 3: "Don't modify migrations" → Claude edits migration → I'm frustratedAfter:
Session N: Hook blocks migration edit → Claude tries alternative → SuccessSession N+1: No reminder needed → Hook still blocks → SuccessSession N+2: No reminder needed → Hook still blocks → SuccessThe rule is no longer mine to remember. It’s the system’s to enforce.
Practical Migration Path
- Audit your reminders: List every rule you’ve told Claude more than twice
- Classify each: Is it architectural? Domain-specific? Task-specific?
- Choose the right home: CLAUDE.md, rules/, hooks, or task brief
- Write it once: As a file, hook, or Non-goals section
- Delete the verbal reminder: Stop saying it
The best constraint is one you never have to think about.
Related Knowledge
- Git Hooks: Similar concept at the version control level
- Linting Rules: Code constraints that fail the build
- Test-Driven Development: Constraints that fail if broken
- Pre-commit Hooks: Last line of defense before commits
These all share the same philosophy: automate what you’d otherwise repeat.
References
- Claude Code Documentation - Official docs on CLAUDE.md and hooks
- CLAUDE.md Best Practices - How to structure project instructions
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments