Skip to content

How to Maintain Invariants and Constraints During Autonomous AI Coding Iteration

Problem

I let Claude Code run autonomously for 50 iterations to improve my codebase. When I came back, the metrics looked great: test coverage up 15%, bundle size down 20%, all tests passing. But then I noticed something was wrong.

Monetary calculations were using float instead of Decimal. The authentication middleware had been “optimized” to skip certain checks. A critical error handling path had been removed because it “wasn’t being used” (it was for a rare but important edge case).

The agent had optimized for the metrics I gave it, but ignored constraints I never explicitly stated. I learned the hard way:

Your CLAUDE.md becomes critical as context grows. If Claude loses track of why a constraint exists, it starts optimizing around it.

What I discovered

After this failure, I found a Reddit discussion about preventing drift in autonomous AI coding. The key insight was defining formal constraints before the agent runs:

“I have a section called ‘invariants’ — things that must stay true no matter what the metric says. Stops a lot of subtle drift before you even notice”

This led to the “Ouro Loop” pattern with three categories of constraints:

  1. IRON LAWS: Invariants that must always hold
  2. DANGER ZONES: Files the agent must never modify blindly
  3. METRICS: How success is measured (but constrained by the above)

The three-layer constraint architecture

Here’s how I now structure constraints in my projects:

┌─────────────────────────────────────────────────────────────────┐
│ Constraint Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IRON LAWS │ │
│ │ - MUST always hold, no exceptions │ │
│ │ - Block any change that violates them │ │
│ │ - Example: All monetary values use Decimal │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DANGER ZONES │ │
│ │ - Require explicit approval before modification │ │
│ │ - High-risk areas prone to subtle breakage │ │
│ │ - Example: auth middleware, payment processing │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ METRICS │ │
│ │ - Define success criteria │ │
│ │ - Can be optimized within constraints │ │
│ │ - Example: coverage >= 80%, bundle < 100KB │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Layer 1: IRON LAWS

These are non-negotiable constraints. No optimization justifies violating them.

CLAUDE.md - IRON LAWS section
## IRON LAWS
These constraints MUST ALWAYS hold. No exceptions. Violations block the entire iteration.
### Financial Accuracy
- All monetary values MUST use `Decimal`, never `float` or `number`
- Currency operations MUST be precise to the cent
- Rounding MUST follow banker's rounding (ROUND_HALF_EVEN)
### Security
- All user inputs MUST be validated before use
- All database queries MUST use parameterized statements
- All secrets MUST come from environment variables, never hardcoded
### Data Integrity
- All database mutations MUST be in transactions
- All writes MUST have corresponding rollback logic
- All deletions MUST be soft-deletes (set deleted_at, never DELETE)
### Performance
- All API endpoints MUST respond within 30 seconds
- All queries MUST have pagination for lists over 100 items
- All heavy operations MUST be async with job queues

Layer 2: DANGER ZONES

These files require explicit approval before modification. The agent must ask, not assume.

CLAUDE.md - DANGER ZONES section
## DANGER ZONES
These files are high-risk. The agent MUST request explicit approval before modifying them.
### Authentication & Authorization
- `src/middleware/auth.ts` - Auth flow, any change could lock out users
- `src/lib/permissions.ts` - Permission logic, bugs = security holes
- `src/lib/session.ts` - Session handling, bugs = auth bypass
### Payment Processing
- `src/services/payment.ts` - Money handling, bugs = financial loss
- `src/services/stripe-webhook.ts` - Webhook handling, bugs = missed payments
- `src/lib/refund.ts` - Refund logic, bugs = money loss
### Data Migration
- `migrations/*.sql` - Schema changes, bugs = data loss
- `src/lib/data-migration.ts` - Data transforms, bugs = corruption
### Critical Infrastructure
- `src/lib/database.ts` - Database connection, bugs = outage
- `src/lib/redis.ts` - Cache layer, bugs = performance collapse
- `src/lib/logger.ts` - Logging, bugs = loss of debugging ability

Layer 3: METRICS

These define what “better” means, but only within the constraints above.

CLAUDE.md - METRICS section
## METRICS
These define success. Optimize for these, but ONLY within IRON LAWS and DANGER ZONES constraints.
### Primary Metrics (optimize for these)
- Test coverage: >= 80% (measure: `npm test -- --coverage`)
- Bundle size: < 100KB (measure: `du -k dist/bundle.js`)
- Type errors: 0 (measure: `npx tsc --noEmit`)
### Secondary Metrics (consider but don't sacrifice primary for these)
- ESLint errors: 0 (measure: `npm run lint`)
- Lighthouse score: >= 90 (measure: `npx lighthouse`)
- API response time: < 200ms p95 (measure: check monitoring dashboard)
### Anti-Metrics (avoid optimizing for these)
- Lines of code (less is not always better)
- Number of tests (quality > quantity)
- Build time (acceptable range: 30s - 120s)

Implementing constraints in CLAUDE.md

Here’s my complete template:

~/.claude/templates/project-constraints.md
# Project Constraints
## IRON LAWS
[Non-negotiable constraints that must ALWAYS hold]
## DANGER ZONES
[Files that require explicit approval before modification]
## METRICS
[How success is measured, constrained by the above]
---
When making changes:
1. First check if the change violates any IRON LAW
2. Then check if the change affects a DANGER ZONE
3. Only then consider if it improves METRICS
If a change violates an IRON LAW: DO NOT MAKE THE CHANGE
If a change affects a DANGER ZONE: ASK FOR APPROVAL FIRST
If a change improves METRICS: PROCEED WITH AUTOMATIC VERIFICATION

Verification script for autonomous iteration

I created a verification script that checks constraints before each iteration:

scripts/verify-constraints.sh
#!/bin/bash
# Verify constraints before accepting a change
set -e
echo "=== Checking IRON LAWS ==="
# Check: No float for monetary values
if grep -rn "float.*price\|float.*amount\|float.*cost\|number.*price\|number.*amount" src/ --include="*.ts" 2>/dev/null; then
echo "VIOLATION: Found float/number for monetary values"
exit 1
fi
# Check: All user inputs are validated (look for z.parse or similar)
if grep -rn "req.body\.\|params\.\|query\." src/ --include="*.ts" 2>/dev/null | grep -v "z.parse\|validate\|sanitize"; then
echo "VIOLATION: Found unvalidated user input"
exit 1
fi
# Check: No hardcoded secrets
if grep -rn "sk-\|api_key.*=\|password.*=\|secret.*=" src/ --include="*.ts" 2>/dev/null | grep -v "process.env"; then
echo "VIOLATION: Found potential hardcoded secrets"
exit 1
fi
echo "=== Checking DANGER ZONES ==="
# Check: If danger zone files changed, require approval
DANGER_FILES=$(git diff --name-only HEAD~1 | grep -E "(auth|payment|migration|database|redis|logger)" || true)
if [ -n "$DANGER_FILES" ]; then
echo "WARNING: Danger zone files modified:"
echo "$DANGER_FILES"
echo "Requires explicit approval to continue"
# In a real implementation, this would prompt for approval
# For CI, this would fail the build
fi
echo "=== All constraints verified ==="

What can go wrong

Mistake 1: Vague constraints

# WRONG: Too vague
- Be careful with money
- Don't break security
- Keep things fast

The agent has no way to verify these programmatically. Use specific, testable constraints:

# CORRECT: Specific and testable
- All monetary values MUST use Decimal type
- All user inputs MUST be validated with zod schemas
- All API endpoints MUST respond within 30 seconds

Mistake 2: Constraints as comments

// WRONG: Constraint in code comment
// IMPORTANT: Always use Decimal for money!
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}

Comments are invisible to the agent when it’s making changes elsewhere. Put constraints in CLAUDE.md:

# CORRECT: Constraint in CLAUDE.md
## IRON LAWS
- All monetary values MUST use Decimal, never float or number

Mistake 3: No constraint verification

# WRONG: Constraints defined but not verified
The agent is told about constraints but nothing enforces them.

Always have a verification step:

Terminal window
# CORRECT: Automated verification
# In iteration loop:
./scripts/verify-constraints.sh || exit 1

Mistake 4: Forgetting why a constraint exists

# WRONG: Constraint without justification
- Use Decimal for money

When context grows, the agent may “optimize around” constraints it doesn’t understand. Add context:

# CORRECT: Constraint with justification
- Use Decimal for money (float precision errors can cause financial discrepancies;
see incident-2024-03-billing-rounding-error)

Advanced patterns

Pattern 1: Constraint categories per domain

Different domains have different constraint priorities:

Domain-Specific Constraints
## Financial Domain
IRON LAWS:
- Decimal for all monetary values
- Audit log for all transactions
- Idempotent payment operations
DANGER ZONES:
- Payment processing files
- Refund logic
- Tax calculation
## Security-Sensitive Domain
IRON LAWS:
- All inputs validated
- All outputs sanitized
- All auth operations logged
DANGER ZONES:
- Authentication middleware
- Permission checks
- Session management
## Performance-Critical Domain
IRON LAWS:
- No N+1 queries
- Pagination on all list endpoints
- Caching headers on static assets
DANGER ZONES:
- Database connection pool
- Cache invalidation logic
- Rate limiting configuration

Pattern 2: Constraint verification in CI

Add constraint checks to your CI pipeline:

.github/workflows/constraints.yml
name: Verify Constraints
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check IRON LAWS
run: |
# No float for monetary values
! grep -rn "float.*price\|float.*amount" src/ --include="*.ts"
# No hardcoded secrets
! grep -rn "sk-\|password.*=" src/ --include="*.ts" | grep -v "process.env"
- name: Check DANGER ZONES modified
run: |
# If danger zones modified, require 'approved-danger-zone' label
CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -E "(auth|payment)" || true)
if [ -n "$CHANGED" ]; then
echo "Danger zone files modified: $CHANGED"
# Check for approval label
LABELS=$(gh pr view --json labels -q '.labels[].name' 2>/dev/null || echo "")
if ! echo "$LABELS" | grep -q "approved-danger-zone"; then
echo "DANGER ZONE modified without approval. Add 'approved-danger-zone' label."
exit 1
fi
fi

Pattern 3: Constraint-aware iteration loop

Modify the autonomous iteration loop to check constraints:

Constraint-Aware Iteration Loop
## Main Loop (repeat until goal achieved or max_iterations)
### Step 1: Check IRON LAWS
Run constraint verification:
```bash
./scripts/verify-constraints.sh

If any violation: REJECT change, try different approach.

Step 2: Check DANGER ZONES

Terminal window
git diff --name-only HEAD~1 | grep -E "(auth|payment|migration)"

If matches found: Request explicit approval before continuing.

Step 3: Verify METRICS improved

Terminal window
new_metric=$( {{metric_command}} )

If metric improved AND constraints satisfied: Keep change.

Step 4: Log with constraint status

iteration | metric | constraints | danger_zones | action
5 | 78% | PASS | NONE | kept
6 | 80% | PASS | auth.ts | awaiting_approval
## Proven use cases
This pattern works well for:
1. **Long-running autonomous iterations**: Prevent drift over many iterations
2. **Team projects**: Constraints encode team knowledge
3. **Safety-critical domains**: Financial, security, healthcare applications
4. **New team members**: Constraints act as documentation
5. **Cross-functional projects**: Different teams can define domain-specific constraints
## Summary
In this post, I showed how to maintain invariants during autonomous AI iteration. The key points are:
1. Define IRON LAWS for non-negotiable constraints
2. Define DANGER ZONES for files requiring explicit approval
3. Define METRICS that respect the above constraints
4. Verify constraints programmatically, not just document them
5. Include justification for each constraint so the agent understands the "why"
This pattern prevents the subtle drift that occurs when an agent optimizes for metrics while forgetting implicit constraints. The explicit constraint hierarchy makes invisible requirements visible to the AI agent.
<FinalWords reflinks={frontmatter.reflinks} />

Comments