A comprehensive guide for developers who want to understand, modify, or extend the ICR plugin.
Acknowledgement: This project is inspired by the YouTube video "The AI Failure Mode Nobody Warned You About (And how to prevent it from happening)". Understanding the "intent problem" described in that video is key to understanding why this codebase exists.
- Prerequisites
- Technology Stack
- Project Structure
- How Claude Code Plugins Work
- Understanding the ICR Flow
- Core Components Deep Dive
- Adding New Features
- Testing Your Changes
- Debugging Tips
- Common Patterns
- API Reference
You should be comfortable with:
- Basic command line operations
- Reading and writing JSON
- Basic shell scripting concepts
Install these before starting:
jq is essential - all ICR scripts use it for JSON processing.
Windows (Git Bash):
# Using Chocolatey
choco install jq
# Verify installation
jq --versionmacOS:
brew install jq
jq --versionLinux (Ubuntu/Debian):
sudo apt-get install jq
jq --versionICR is a Claude Code plugin. Install Claude Code from [Anthropic's documentation].
Any editor works. VS Code is recommended for:
- Bash syntax highlighting
- JSON validation
- Markdown preview
- ShellCheck: Lints bash scripts for errors
# macOS brew install shellcheck # Ubuntu sudo apt-get install shellcheck
ICR uses Bash scripts because:
- Claude Code hooks execute shell commands - Native integration
- Cross-platform - Works on macOS, Linux, WSL on Windows
- No compilation needed - Edit and run immediately
- Minimal dependencies - Only requires jq
| Technology | Purpose | Where Used |
|---|---|---|
| Bash | Script execution | All .sh files |
| jq | JSON parsing/generation | All JSON operations |
| JSON | Data format | Config, receipts, schemas |
| JSON Schema | Validation | schemas/*.json |
| Markdown | Documentation & commands | commands/*.md, skills/*.md |
If you're new to Bash from C/C++/Java, here are key differences:
# Variables (no type declaration)
my_var="hello"
echo "$my_var" # Always quote variables
# Command substitution (like backticks in old style)
result=$(some_command)
# Conditionals
if [[ "$var" == "value" ]]; then
echo "Match"
fi
# Functions
my_function() {
local param="$1" # Local variables
echo "Got: $param"
return 0 # Return exit code (0 = success)
}
# Pipes (pass output to next command)
echo '{"key": "value"}' | jq '.key' # Outputs: "value"icr/
├── .claude-plugin/
│ └── plugin.json # 1. PLUGIN MANIFEST
│
├── hooks/
│ └── hooks.json # 2. HOOK REGISTRATION
│
├── scripts/ # 3. HOOK IMPLEMENTATIONS
│ ├── pre-tool-check.sh # Main entry point
│ ├── permission-check.sh # Permission dialog augmentation
│ ├── subagent-check.sh # Subagent review
│ ├── statusline.sh # Status line display
│ └── lib/ # Core library
│ ├── common.sh # Shared utilities
│ ├── severity.sh # Severity classification
│ ├── confidence.sh # Confidence calculation
│ ├── decision.sh # Decision tree
│ ├── intent.sh # Intent generation
│ ├── receipt.sh # Receipt logging
│ └── checkpoint.sh # Checkpoint management
│
├── commands/ # 4. SLASH COMMANDS
│ ├── receipts.md
│ ├── config.md
│ └── ...
│
├── skills/ # 5. AI SKILLS
│ └── intent-analysis/
│ └── SKILL.md
│
├── config/ # 6. CONFIGURATION
│ └── defaults.json
│
├── schemas/ # 7. VALIDATION
│ ├── config.json
│ ├── receipt.json
│ └── intent-doc.json
│
└── prompts/ # 8. LLM PROMPTS
├── intent-generation.md
├── self-critique.md
└── devils-advocate.md
┌─────────────────────────────────────────────────────────────────────┐
│ PLUGIN.JSON │
│ (Tells Claude Code what files to use) │
└─────────────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HOOKS │ │ COMMANDS │ │ SKILLS │
│ │ │ │ │ │
│hooks.json│ │ *.md │ │ SKILL.md │
└────┬─────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ SCRIPTS │
│ │
│ *.sh │
└────┬─────┘
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ LIB │─────▶│ CONFIG │ │ PROMPTS │
│ │ │ │ │ │
│lib/*.sh │ │defaults │ │ *.md │
└──────────┘ └──────────┘ └──────────┘
Claude Code finds plugins in:
~/.claude/plugins/(global)./.claude-plugins/(project local)- Paths specified with
--plugin-dir
plugin.json is the entry point:
{
"name": "icr",
"version": "1.0.0",
"description": "Intent-Check-Receipt safety plugin",
"hooks": {
"PreToolUse": {
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-check.sh"
}
},
"commands": [
{
"name": "icr:receipts",
"path": "commands/receipts.md",
"description": "View receipt history"
}
],
"skills": [
{
"name": "intent-analysis",
"path": "skills/intent-analysis/SKILL.md"
}
]
}Claude Code emits events that plugins can intercept:
| Event | When Fired | What Plugin Can Do |
|---|---|---|
PreToolUse |
Before any tool runs | Block, allow, or modify |
PostToolUse |
After tool completes | Log, verify, cleanup |
PermissionRequest |
When asking user | Add context to dialog |
SubagentStop |
When subagent finishes | Review aggregate work |
Hooks receive JSON via stdin and output JSON to stdout:
Input (from Claude Code):
{
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/test"
},
"session_id": "abc123"
}Output (to Claude Code):
{
"decision": "proceed",
"reason": "User approved",
"receipt_id": "550e8400-..."
}Or to block:
{
"decision": "block",
"reason": "User aborted action"
}┌──────────────────────────────────────────────────────────────────────────┐
│ USER REQUEST │
│ "Clean up old files in /tmp" │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ CLAUDE CODE │
│ Interprets request, prepares tool call │
│ Tool: Bash, Command: "find /tmp -mtime +30 -delete" │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ PreToolUse HOOK FIRED │
│ │
│ Claude Code sends: │
│ { │
│ "tool_name": "Bash", │
│ "tool_input": {"command": "find /tmp -mtime +30 -delete"}, │
│ "session_id": "abc123", │
│ "user_prompt": "Clean up old files in /tmp" │
│ } │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ pre-tool-check.sh │
│ │
│ STEP 1: Is this tool excluded? │
│ Check config.hooks.preToolUse.excludeTools │
│ If excluded → return {decision: "proceed"} │
│ │
│ STEP 2: Load configuration │
│ Merge defaults.json + user config.json │
│ │
│ STEP 3: Classify severity │
│ Call lib/severity.sh │
│ Result: CRITICAL (contains "delete") │
│ │
│ STEP 4: Generate intent document │
│ Call lib/intent.sh │
│ Result: Structured 7-field document │
│ │
│ STEP 5: Calculate confidence │
│ Call lib/confidence.sh │
│ Result: 0.62 (medium confidence) │
│ │
│ STEP 6: Make decision │
│ Call lib/decision.sh │
│ CRITICAL + 0.62 → HUMAN_REVIEW │
│ │
│ STEP 7: Create receipt │
│ Call lib/receipt.sh │
│ Save to .claude/icr/receipts/... │
│ │
│ STEP 8: Return decision │
│ {decision: "pending", route: "HUMAN_REVIEW", ...} │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ USER SEES PROMPT │
│ │
│ ICR INTENT CHECK │
│ ================ │
│ Task: Delete files in /tmp older than 30 days │
│ ... │
│ [1] Proceed [2] Edit [3] Abort [4] Explain [5] Bypass │
└──────────────────────────────────────────────────────────────────────────┘
│
User presses [1]
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ RECEIPT UPDATED │
│ │
│ lib/receipt.sh update │
│ - humanResponse: "PROCEED" │
│ - humanResponseTime: 2300ms │
│ - outcome: "proceeded" │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ TOOL EXECUTES │
│ │
│ Claude Code runs the Bash command │
│ Files are deleted │
└──────────────────────────────────────────────────────────────────────────┘
Location: scripts/lib/common.sh
This file provides utilities used by all other scripts:
# --- LOGGING ---
log_debug "Detailed info" # Only shown if DEBUG=1
log_info "Normal info" # Standard logging
log_warn "Warning message" # Warnings
log_error "Error message" # Errors
# --- CONFIGURATION ---
load_config # Returns merged config JSON
is_icr_enabled "$config" # Check if ICR is enabled
# --- JSON HELPERS ---
read_json_input # Read JSON from stdin
json_output "key" "value" # Create simple JSON object
# --- SESSION ---
get_session_id # Get or create session ID
# --- REQUIREMENTS ---
require_jq # Exit if jq not installedExample usage:
#!/bin/bash
source "${LIB_DIR}/common.sh"
main() {
require_jq
local config
config=$(load_config)
log_info "Loaded config"
local input
input=$(read_json_input)
log_debug "Input: $input"
}Location: scripts/lib/severity.sh
Classifies actions into LOW, MEDIUM, HIGH, or CRITICAL.
Three-Layer Approach:
Layer 1: Static Rules (tool name → severity)
│
▼
Layer 2: Metadata Inference (analyze arguments)
│
▼
Layer 3: User Rules (custom patterns override)
Key Functions:
# Static classification based on tool name
classify_static "$tool_name"
# Infer from command/arguments
infer_from_metadata "$tool_name" "$tool_input"
# Apply user-defined rules
apply_user_rules "$tool_name" "$tool_input" "$config"
# Main entry - combines all layers
classify_severity "$tool_name" "$tool_input" "$config"Input (via stdin):
{
"tool_name": "Bash",
"tool_input": {"command": "rm -rf /"},
"config": {...}
}Output:
{
"severity": "CRITICAL",
"classifiedBy": "metadata",
"reason": "Command contains 'rm -rf'"
}Location: scripts/lib/confidence.sh
Calculates confidence score (0.0 - 1.0) from four signals.
The Four Signals:
┌─────────────────────────────────────────────────────────────────┐
│ CONFIDENCE SCORE │
│ │
│ Ambiguity (30%) ─┐ │
│ │ │
│ Distance (25%) ─┼──▶ Weighted Average ──▶ Final Score │
│ │ │
│ Historical (20%) ─┤ │
│ │ │
│ Uncertainty (25%)─┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Functions:
# Count valid interpretations
analyze_ambiguity "$user_prompt"
# Measure semantic gap
calculate_distance "$user_prompt" "$tool_name" "$tool_input"
# Check past patterns
analyze_historical "$tool_name" "$tool_input" "$receipts_dir"
# Detect hedging language
analyze_uncertainty "$user_prompt"
# Main entry
calculate_confidence "$user_prompt" "$tool_name" "$tool_input" "$config" "$receipts_dir"Output:
{
"overall": 0.68,
"components": {
"ambiguity": {"score": 0.55, "reason": "..."},
"distance": {"score": 0.75, "reason": "..."},
"historical": {"score": 0.70, "reason": "..."},
"uncertainty": {"score": 0.72, "reason": "..."}
}
}Location: scripts/lib/decision.sh
Determines the route: AUTO_APPROVE, AI_REVIEW, or HUMAN_REVIEW.
Decision Logic:
IF manual_check_flag == true:
RETURN HUMAN_REVIEW
IF trust_mode == true AND severity != CRITICAL:
RETURN AUTO_APPROVE_TRUST_MODE
thresholds = get_thresholds(severity)
IF confidence >= thresholds.autoApprove:
RETURN AUTO_APPROVE
IF confidence >= thresholds.aiReview:
RETURN AI_REVIEW
RETURN HUMAN_REVIEW
Key Functions:
# Get thresholds for a severity level
get_threshold "$severity" "$config"
# Check trust mode status
is_trust_mode_enabled "$session_state"
# Main decision function
make_decision "$severity" "$confidence" "$config" "$session_state" "$manual_check"Location: scripts/lib/intent.sh
Generates the 7-field intent document.
The Seven Fields:
{
"task": "One-sentence description",
"whoWhat": {
"affected": ["what will change"],
"excluded": ["what won't change"]
},
"boundaries": ["what the action won't do"],
"ifUncertain": "fallback behavior",
"reversibility": {
"canUndo": true,
"method": "how to undo",
"effort": "trivial|moderate|significant|impossible"
},
"alternatives": [
{"interpretation": "...", "outcome": "...", "whyNotChosen": "..."}
],
"confidenceBreakdown": {...}
}Key Functions:
# Analyze what resources are affected
analyze_affected "$tool_name" "$tool_input"
# Assess reversibility
analyze_reversibility "$tool_name" "$tool_input"
# Generate boundary statements
generate_boundaries "$tool_name" "$tool_input"
# Main generation function
generate_intent "$user_prompt" "$tool_name" "$tool_input" "$confidence_result"
# Format for display
format_intent_display "$intent_doc" "$severity" "$confidence"Location: scripts/lib/receipt.sh
Manages the audit trail.
Receipt Structure:
.claude/icr/receipts/
├── index.json # Fast lookup index
├── 2026-01-02/
│ └── session-abc123/
│ ├── session-meta.json
│ ├── 001-receipt.json
│ ├── 002-receipt.json
│ └── ...
Key Functions:
# Initialize receipts directory
init_receipts_dir
# Get next sequence number
get_next_sequence "$session_id"
# Create a new receipt
create_receipt # Input via stdin
# Update existing receipt
update_receipt "$receipt_id" "$updates"
# Clean old receipts (based on retention)
cleanup_old_receipts "$config"Location: scripts/lib/checkpoint.sh
Manages checkpoints for rollback.
Key Functions:
# Should we create a checkpoint?
should_create_checkpoint "$severity" "$config"
# Create a checkpoint
create_checkpoint "$tool_name" "$severity" "$receipt_id" "$config"
# Link checkpoint to receipt
link_checkpoint_to_receipt "$checkpoint_id" "$receipt_path" "$config"
# Suggest revert after problems
suggest_revert "$receipt_id"Step 1: Create the command file
Create commands/mycommand.md:
---
description: Description shown in help
user_invocable: true
---
# My Command
Documentation for the command.
## Usage
- `/icr:mycommand` - Basic usage
- `/icr:mycommand arg` - With argument
## What It Does
Explanation of behavior.Step 2: Register in plugin.json
Add to the commands array:
{
"commands": [
{
"name": "icr:mycommand",
"path": "commands/mycommand.md",
"description": "Short description"
}
]
}Step 3: Test
/icr:mycommandOption 1: Add to defaults.json
{
"severity": {
"staticRules": {
"NewTool": "HIGH"
}
}
}Option 2: Add metadata inference
Edit scripts/lib/severity.sh:
infer_from_metadata() {
local tool_name="$1"
local tool_input="$2"
# Add your pattern
if [[ "$tool_name" == "NewTool" ]]; then
local dangerous_param
dangerous_param=$(echo "$tool_input" | jq -r '.dangerous // ""')
if [[ "$dangerous_param" == "true" ]]; then
echo '{"severity": "CRITICAL", "reason": "Dangerous parameter set"}'
return
fi
fi
# ... rest of function
}Step 1: Create the script
Create scripts/my-hook.sh:
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
main() {
require_jq
local input
input=$(read_json_input)
# Your logic here
log_info "My hook received: $input"
# Must output JSON
echo '{"decision": "proceed"}'
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fiStep 2: Make executable
chmod +x scripts/my-hook.shStep 3: Register in hooks.json
{
"hooks": {
"MyEvent": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.sh"
}
]
}
]
}
}Edit config/defaults.json:
{
"confidence": {
"weights": {
"ambiguityAnalysis": 0.35, // Increase ambiguity weight
"intentToActionDistance": 0.25,
"historicalPatterns": 0.15, // Decrease historical weight
"uncertaintyMarkers": 0.25
}
}
}Test a single script:
# Test severity classification
echo '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}' | \
bash scripts/lib/severity.sh
# Test confidence calculation
bash scripts/lib/confidence.sh \
"delete old files" \
"Bash" \
'{"command": "rm"}' \
'{}' \
".claude/icr/receipts"Test the full hook:
echo '{
"tool_name": "Bash",
"tool_input": {"command": "ls -la"},
"session_id": "test",
"user_prompt": "list files"
}' | bash scripts/pre-tool-check.sh# Start Claude Code with the plugin
claude --plugin-dir ./icr
# Run commands
/icr:stats
/icr:simulate Bash "echo test"Use ShellCheck:
shellcheck scripts/*.sh
shellcheck scripts/lib/*.shCheck JSON validity:
jq . config/defaults.json
jq . schemas/config.jsonexport DEBUG=1
# Now log_debug messages will appearbash scripts/lib/severity.sh <<< '{"tool_name": "Read"}'
echo "Exit code: $?"bash scripts/lib/severity.sh <<< '{"tool_name": "Bash"}' | jq ."jq: command not found"
- Install jq (see Prerequisites)
"parse error: Invalid numeric literal"
- Your JSON is malformed. Check quotes and commas.
"command not found: source"
- Use
bashexplicitly:bash scripts/pre-tool-check.sh
Permission denied
- Make scripts executable:
chmod +x scripts/*.sh
local input
input=$(cat) # or $(read_json_input) from common.sh
local tool_name
tool_name=$(echo "$input" | jq -r '.tool_name')# Simple object
jq -n --arg key "$value" '{"key": $key}'
# Complex object
cat <<EOF
{
"field1": "$value1",
"field2": $numeric_value,
"nested": {
"inner": "$inner_value"
}
}
EOF# Always quote variables
echo "$my_var"
# Default values
local value="${my_var:-default}"
# Check if empty
if [[ -z "$my_var" ]]; then
echo "Variable is empty"
fi# Strict mode (at top of script)
set -euo pipefail
# Catch errors
if ! result=$(some_command); then
log_error "Command failed"
exit 1
fi
# Try with fallback
result=$(risky_command 2>/dev/null || echo "default")All hooks receive JSON via stdin:
{
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {...},
"session_id": "abc123",
"conversation_turn": 5,
"user_prompt": "original request",
"conversation_context": "..."
}Allow action:
{
"decision": "proceed",
"reason": "Why allowed",
"receipt_id": "uuid",
"createCheckpoint": true,
"checkpointLabel": "ICR: Before Bash"
}Block action:
{
"decision": "block",
"reason": "Why blocked",
"receipt_id": "uuid"
}Request human review:
{
"decision": "pending",
"route": "HUMAN_REVIEW",
"receipt_id": "uuid",
"intent": {...},
"display": "formatted text",
"options": [
{"key": "1", "label": "Proceed", "action": "proceed"},
{"key": "2", "label": "Edit", "action": "edit"},
{"key": "3", "label": "Abort", "action": "abort"}
]
}See schemas/config.json for the complete schema.
Key sections:
severity: Classification rulesconfidence: Scoring weightsthresholds: Decision thresholds per severityhooks: Hook-specific settingsreceipts: Logging settingsbypass: Bypass controlserrorHandling: Fallback behavior
- Architecture Overview: ARCHITECTURE.md
- Contributing: CONTRIBUTING.md
- Troubleshooting: TROUBLESHOOTING.md
- Full Specification: ICR_PLUGIN_SPEC.md