Skip to content

Latest commit

 

History

History
1108 lines (859 loc) · 30.4 KB

File metadata and controls

1108 lines (859 loc) · 30.4 KB

ICR Developer Guide

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.

Table of Contents

  1. Prerequisites
  2. Technology Stack
  3. Project Structure
  4. How Claude Code Plugins Work
  5. Understanding the ICR Flow
  6. Core Components Deep Dive
  7. Adding New Features
  8. Testing Your Changes
  9. Debugging Tips
  10. Common Patterns
  11. API Reference

Prerequisites

Required Knowledge

You should be comfortable with:

  • Basic command line operations
  • Reading and writing JSON
  • Basic shell scripting concepts

Required Tools

Install these before starting:

1. jq (JSON Processor)

jq is essential - all ICR scripts use it for JSON processing.

Windows (Git Bash):

# Using Chocolatey
choco install jq

# Verify installation
jq --version

macOS:

brew install jq
jq --version

Linux (Ubuntu/Debian):

sudo apt-get install jq
jq --version

2. Claude Code

ICR is a Claude Code plugin. Install Claude Code from [Anthropic's documentation].

3. A Good Text Editor

Any editor works. VS Code is recommended for:

  • Bash syntax highlighting
  • JSON validation
  • Markdown preview

Optional but Helpful

  • ShellCheck: Lints bash scripts for errors
    # macOS
    brew install shellcheck
    
    # Ubuntu
    sudo apt-get install shellcheck

Technology Stack

Why Bash?

ICR uses Bash scripts because:

  1. Claude Code hooks execute shell commands - Native integration
  2. Cross-platform - Works on macOS, Linux, WSL on Windows
  3. No compilation needed - Edit and run immediately
  4. Minimal dependencies - Only requires jq

Technologies Used

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

Key Bash Concepts

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"

Project Structure

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

How Files Connect

┌─────────────────────────────────────────────────────────────────────┐
│                        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     │
     └──────────┘      └──────────┘      └──────────┘

How Claude Code Plugins Work

Plugin Discovery

Claude Code finds plugins in:

  1. ~/.claude/plugins/ (global)
  2. ./.claude-plugins/ (project local)
  3. Paths specified with --plugin-dir

Plugin Manifest

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"
    }
  ]
}

Hook Events

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

Hook I/O Protocol

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"
}

Understanding the ICR Flow

The Complete Flow Diagram

┌──────────────────────────────────────────────────────────────────────────┐
│                          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                                                       │
└──────────────────────────────────────────────────────────────────────────┘

Core Components Deep Dive

1. common.sh - Shared Utilities

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 installed

Example 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"
}

2. severity.sh - Severity Classification

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'"
}

3. confidence.sh - Confidence Scoring

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": "..."}
  }
}

4. decision.sh - Decision Tree

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"

5. intent.sh - Intent Document Generation

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"

6. receipt.sh - Receipt Logging

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"

7. checkpoint.sh - Checkpoint Management

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"

Adding New Features

Adding a New Slash Command

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:mycommand

Adding a New Severity Rule

Option 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
}

Adding a New Hook

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 "$@"
fi

Step 2: Make executable

chmod +x scripts/my-hook.sh

Step 3: Register in hooks.json

{
  "hooks": {
    "MyEvent": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.sh"
          }
        ]
      }
    ]
  }
}

Modifying Confidence Weights

Edit config/defaults.json:

{
  "confidence": {
    "weights": {
      "ambiguityAnalysis": 0.35,      // Increase ambiguity weight
      "intentToActionDistance": 0.25,
      "historicalPatterns": 0.15,     // Decrease historical weight
      "uncertaintyMarkers": 0.25
    }
  }
}

Testing Your Changes

Manual Testing

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

Using Claude Code

# Start Claude Code with the plugin
claude --plugin-dir ./icr

# Run commands
/icr:stats
/icr:simulate Bash "echo test"

Checking for Errors

Use ShellCheck:

shellcheck scripts/*.sh
shellcheck scripts/lib/*.sh

Check JSON validity:

jq . config/defaults.json
jq . schemas/config.json

Debugging Tips

Enable Debug Logging

export DEBUG=1
# Now log_debug messages will appear

Check Script Exit Codes

bash scripts/lib/severity.sh <<< '{"tool_name": "Read"}'
echo "Exit code: $?"

Validate JSON Output

bash scripts/lib/severity.sh <<< '{"tool_name": "Bash"}' | jq .

Common Errors

"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 bash explicitly: bash scripts/pre-tool-check.sh

Permission denied

  • Make scripts executable: chmod +x scripts/*.sh

Common Patterns

Reading JSON from Stdin

local input
input=$(cat)  # or $(read_json_input) from common.sh

local tool_name
tool_name=$(echo "$input" | jq -r '.tool_name')

Creating JSON Output

# Simple object
jq -n --arg key "$value" '{"key": $key}'

# Complex object
cat <<EOF
{
  "field1": "$value1",
  "field2": $numeric_value,
  "nested": {
    "inner": "$inner_value"
  }
}
EOF

Safe Variable Handling

# 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

Error Handling

# 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")

API Reference

Hook Input Format

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": "..."
}

Hook Output Format

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"}
  ]
}

Configuration Schema

See schemas/config.json for the complete schema.

Key sections:

  • severity: Classification rules
  • confidence: Scoring weights
  • thresholds: Decision thresholds per severity
  • hooks: Hook-specific settings
  • receipts: Logging settings
  • bypass: Bypass controls
  • errorHandling: Fallback behavior

Getting Help