Module 5: On-Demand Loading & Drift Detection¶
Time: 20 minutes Goal: Add tier2 keyword triggers, cross-check drift detection, PostToolUse actions, and the stop hook.
Using SQLite instead of YAML?
This module shows YAML examples. If you chose SQLite in the setup wizard, see the Data Store Mapping Guide for equivalent database commands.
Part A: Tier 2 On-Demand Loading¶
The Concept¶
Tier 1 files load at startup — every session, unconditionally. Tier 2 files load on demand when the agent's tool calls contain specific keywords. This keeps startup fast while ensuring specialized rules (e.g., API protocols, case extraction steps) are available when needed.
The gate scans tool input for trigger keywords. When a match is found, the gate blocks the tool call and tells the agent to read the tier 2 file first. Once read, the keyword is satisfied for the rest of the session.
Step 1: Create Tier 2 Rule Files¶
Add your tier 2 content as separate files and register them in
startup-config.yaml with a triggers array:
tier2:
- name: api-rules
path: rules/api-rules.md
triggers: ["api", "endpoint", "REST"]
- name: case-extraction
path: rules/case-extraction.md
triggers: ["case", "extraction", "support-ticket"]
- name: research-protocol
path: rules/research-protocol.md
triggers: ["research", "synthesis", "report"]
Each entry needs a unique name, a path to the rule file, and a
triggers array of keywords that activate it.
Step 2: Enable in Gates Config¶
Update the gates section of startup-config.yaml:
gates:
block_until_tier1: true
tier2_keyword_scan: true # ← Enable tier 2 scanning
keyword_scan_fields: # Which tool input fields to scan
- file_path
- command
- description
keyword_scan_max_chars: 120 # Scan only first N chars (performance)
keyword_scan_fields— limits scanning to specific input fields instead of the entire tool payloadkeyword_scan_max_chars— caps how much of each field is scanned, preventing slowdowns on large inputs
Step 3: Test It¶
- Start a new session and let tier 1 complete normally
- Run a command containing a trigger keyword:
- The gate blocks with:
- Agent reads the file → gate unblocks → original tool call succeeds
Step 4: Trigger Design Tips¶
- Be specific. Prefer
"extraction"over"extract"— short words cause false positives on unrelated commands. - Test against common commands. Run
git status,ls,cat— if any trigger fires, the keyword is too broad. - Use 2-3 triggers per file. More triggers increase match odds but also increase false-positive risk.
- Lowercase matching. The scanner lowercases both input and triggers,
so
"API"and"api"are equivalent — list only one.
Part B: Cross-Check Drift Detection¶
The Concept¶
After tier1 loads, run a one-time check comparing expected state (from your config) against actual state (from live commands). This catches drift that would otherwise go unnoticed.
Step 1: Define Expectations¶
Add to startup-config.yaml:
cross_check:
expected_counts:
rule_file_count:
command: "find rules/ -name '*.md' | wc -l | tr -d ' '"
expected: 3
auto_heal: false
test_count:
command: "find tests/ -name 'test_*.py' | wc -l | tr -d ' '"
expected: 12
auto_heal: false
Step 2: How It Runs¶
The gate_check.py script automatically invokes cross_check.py after tier1 completes — once per session:
- Pass 1: Run each command, compare output to expected value
- Auto-heal safe items (if configured with
auto_heal: true) - Pass 2: Re-check healed items to confirm the fix worked
- Write-back suggestions: For persistent drift, generate
write_back_suggestionsproposing manifest updates or flagging items for investigation (structurally enforces the Self-Healing Loop pattern) - Stop — no more passes (bounded, prevents infinite loops)
Results are logged to the sentinel and reported:
Or if drift is found:
When to Use Auto-Heal¶
- Safe: Updating a count in a config file
- Unsafe: Deleting files, modifying code, running migrations
- Rule: If the fix could cause damage, set
auto_heal: falseand let the human decide
Part C: PostToolUse Hook¶
The Concept¶
After every Write or Edit, run automated actions: - Sync important files to backup locations - Track edit count for save reminders - Detect stale references (advanced)
Setup¶
If you used setup.py with Level 4, this is already installed. Otherwise:
Add to .agent/settings.json:
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python3 .agent/hooks/on_edit.py",
"timeout": 5000
}]
}
]
The default on_edit.py provides periodic save reminders (every 15 edits) and structurally enforces the Rule Zero pattern: it scans edited files for keyword overlap with consolidated files and warns if content appears scattered. Customize it to add file sync or other post-write actions.
DB mode: When AGENT_DB_PATH is set, every edit is also logged to
the rule_log table — enabling session activity reports and edit count
tracking at shutdown.
Part D: Stop Hook¶
The Concept¶
Block session exit until cleanup is done. The stop hook returns exit code 2 (retry) when checks fail, giving Claude a chance to fix the issue. After max retries, it exits cleanly. The stop hook also structurally enforces Self-Verification: if infrastructure files were edited after the last infrastructure check, exit is blocked until re-verification runs.
Setup¶
Add to config:
stop:
require_clean_repos: true
require_transcript: false
require_audit_pass: true # run audit checks before exit
max_retries: 8
shutdown_steps: # custom checks before exit (same validators as startup)
- name: lint-clean
command: "npm run lint 2>&1 | tail -1"
validator: "contains:no errors"
fail_message: "Linter has errors"
shutdown_steps uses the same validator registry as startup checks
(empty_output, contains:text, equals:text, regex:pattern).
Each step runs before exit; failures trigger retry (exit code 2).
DB mode: Add require_session_summary: true to require a
session_summaries row before exit — ensures session learnings are captured.
Wire in settings:
"Stop": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "python3 .agent/hooks/on_stop.py",
"timeout": 10000
}]
}
]
How Retries Work¶
graph TD
A["Session ending"] --> B["on_stop.py checks repos"]
B --> C{Repos clean?}
C -->|YES| D["Exit 0 — allow exit"]
C -->|NO| E["Exit 2 — retry"]
E --> F["Agent sees: 'Repos not clean'"]
F --> G["Agent runs: git add + git commit"]
G --> B
B --> H{Max retries?}
H -->|reached| I["Force exit — don't trap user"]
style D fill:#5cb85c,color:#fff
style E fill:#d9534f,color:#fff
style I fill:#f0ad4e,color:#fff
Max retries prevents the user from being trapped if the check can't be satisfied.
Audit Runner Integration¶
The stop hook can also run the Audit Runner
before allowing exit. Enable with require_audit_pass: true in your config.
This catches infrastructure drift introduced during the session.
Every audit check was born from a real failure:
graph LR
A["Something<br/>goes wrong"] --> B["Learning<br/>logged"]
B --> C["Becomes<br/>a rule"]
C --> D["Add audit<br/>check"]
D --> E["Wire into<br/>stop hook"]
style A fill:#d9534f,color:#fff
style D fill:#5cb85c,color:#fff
style E fill:#9b59b6,color:#fff
See the full check library for all 22 checks with "What It Catches / Why It Matters" tables.
DB mode note: When using SQLite, the stop hook also enforces no-truncation — it verifies stored rule content length matches the source, blocking exit if any rule was silently truncated during a batch insert.
What You've Built (Full Level 4)¶
Session start → SessionStart hook
→ Checks infrastructure
→ Generates tier1 + tier2 files
→ Writes manifest + sentinel
│
Every user message → UserPromptSubmit hook
→ Blocks until tier1 complete
→ Warns at prompt thresholds
│
Every tool call → PreToolUse hook
→ Blocks non-Read until tier1 complete
→ Auto-allows git commit/push (version control never blocked)
→ Runs cross-check once
→ Triggers tier2 on keywords
│
Every Write/Edit → PostToolUse hook
→ Save reminders
→ Rule Zero enforcement (scattered content detection)
→ File sync (if configured)
│
Session end → Stop hook
→ Blocks until repos clean
→ Runs audit checks (if require_audit_pass)
→ Self-verification enforcement
→ Retries up to N times
Part E: Session Continuity¶
Agent sessions are ephemeral — tasks, progress, and "what's next" vanish when the session ends. Session continuity solves this with two features:
- Persistent Backlog — todos stored in a file or database that survives across sessions, loaded into Tier 1 at startup
- Session Handoff — each session records what was done and what's next, so the following session picks up where the last one left off
Why this matters: Without continuity, users re-explain context every session. With it, the agent says "Last session worked on auth refactor. Next items: fix flaky test, update docs" — and starts working.
Two implementation methods: - JSON file — simple, zero dependencies, good for under ~20 tasks - SQLite — queryable, handles concurrency, tracks history
See the full Session Continuity Guide for setup instructions, code examples for both methods, and stop hook integration for automatic session handoff.
Checkpoint¶
- [ ] Tier 2 triggers fire when keywords appear in commands
- [ ] Cross-check runs after tier1 completes (check sentinel for
cross_check_done: true) - [ ] Save reminder appears after 15 edits
- [ ] Stop hook blocks exit when repos are dirty
- [ ] Backlog loads in tier1 at session start (JSON or SQLite method)