Skip to content

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:

  1. Context overflow: Long conversations bury early instructions
  2. Token cost: Repeating rules wastes context window
  3. No enforcement: AI can acknowledge a rule but still violate it
  4. 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:

CLAUDE.md
## 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 lint

Keep this under 200 lines. It’s loaded into every session, so density matters.

.claude/rules/*.md - Domain-specific rules:

.claude/rules/migrations.md
# 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:migrate

These 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 changes

The “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 value

Executable 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 it
Session 2: "Don't modify migrations" → Claude suggests migration edit → I reject it
Session 3: "Don't modify migrations" → Claude edits migration → I'm frustrated

After:

Session N: Hook blocks migration edit → Claude tries alternative → Success
Session N+1: No reminder needed → Hook still blocks → Success
Session N+2: No reminder needed → Hook still blocks → Success

The rule is no longer mine to remember. It’s the system’s to enforce.

Practical Migration Path

  1. Audit your reminders: List every rule you’ve told Claude more than twice
  2. Classify each: Is it architectural? Domain-specific? Task-specific?
  3. Choose the right home: CLAUDE.md, rules/, hooks, or task brief
  4. Write it once: As a file, hook, or Non-goals section
  5. Delete the verbal reminder: Stop saying it

The best constraint is one you never have to think about.

  • 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

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