Skillzwave

Claude Code Hooks Implementation: Audit System

Updated
15 min read

In today's complex development environments, accountability is essential. This guide walks you through creating a robust audit system that automatically logs all interactions, generates structured audit trails for SOX/SOC2 compliance, and provides complete traceability.

Audit system architecture showing hooks, logging, and compliance tracking
Transform Claude into a fully auditable teammate with complete traceability

In This Guide

  • 1. Understanding hook event types and architecture
  • 2. Step-by-step implementation of the audit logger
  • 3. Creating slash commands for session management
  • 4. Enterprise best practices and compliance

Understanding Hook Event Types

Claude Code hooks are your entry point for intercepting actions inside the development environment. Think of them as checkpoints that let you run custom logic before or after a tool executes. This forms the bedrock of our audit system.

Key Hook Events for Auditing

PreToolUse Fires before any tool runs. Perfect for logging the intended action.
PostToolUse Fires after a tool completes. Ideal for capturing results.
Stop Marks the end of a session. Perfect for final summaries.
UserPromptSubmit Captures the raw prompt from the user.

Critical Note: Official Claude Code documentation shows that hooks receive a JSON payload via stdin. They do not use environment variables for context.

Hook Architecture and Response

When an action triggers our PreToolUse hook, the system pauses and waits for our script's response. Our script logs the action and then tells Claude to proceed.

Flow diagram showing hook intercept, log, and response pattern
The basic flow: intercept, log, respond with continue:true

To allow execution, our script simply needs to print {"continue": true} to standard output. Simple, right?

Step 1: Create the Hook Directory Structure

First, set up the directories for your global hooks and local project logs:

# Create the global Claude hooks directory
mkdir -p ~/.claude/hooks

# Create project-specific audit directories
mkdir -p debugging/audit
mkdir -p debugging/audit/archive

Step 2: Create the Audit Logger Script

This script is the heart of our system. It captures every tool invocation, formats the data, and writes it to our active log file.

# Create the script file
touch ~/.claude/hooks/audit_logger.sh

# Make it executable
chmod +x ~/.claude/hooks/audit_logger.sh

The Complete Audit Logger Script

Create ~/.claude/hooks/audit_logger.sh:

#!/usr/bin/env bash
set -e

# Function to get the current audit file
get_current_audit_file() {
    if [ -f "$HOME/.claude/current_audit_file" ]; then
        cat "$HOME/.claude/current_audit_file"
    else
        # Create default audit file if none exists
        TIMESTAMP=$(date +"%Y_%m_%d-%H_%M")
        DEFAULT_AUDIT="debugging/audit/audit_$TIMESTAMP-default.md"
        echo "$DEFAULT_AUDIT" > "$HOME/.claude/current_audit_file"
        echo "$DEFAULT_AUDIT"
    fi
}

# Function to ensure audit file exists with header
ensure_audit_file() {
    local AUDIT_FILE="$1"
    if [ ! -f "$AUDIT_FILE" ]; then
        mkdir -p "$(dirname "$AUDIT_FILE")" 2>/dev/null || true
        {
            echo "# Audit Log"
            echo
            echo "**Session Started:** $(date '+%Y-%m-%d %H:%M:%S')"
            echo "**User:** $(whoami)"
            echo "**Working Directory:** $(pwd)"
            echo "**Project:** $(basename "$(pwd)")"
            echo
            echo "---"
            echo
            echo "## Activity Log"
            echo
        } > "$AUDIT_FILE"
    fi
}

# Read input from stdin
INPUT=$(cat)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# Get audit file and ensure it exists
AUDIT_FILE=$(get_current_audit_file)
ensure_audit_file "$AUDIT_FILE"

# Extract tool information using jq
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "Unknown"')

# Log different tool types
case "$TOOL_NAME" in
    "Bash")
        COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
        DESCRIPTION=$(echo "$INPUT" | jq -r '.tool_input.description // ""')
        {
            echo "### Tool Invocation: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            echo
            echo "**Command:**"
            echo '```bash'
            echo "$COMMAND"
            echo '```'
            [ -n "$DESCRIPTION" ] && echo "**Purpose:** $DESCRIPTION"
            echo
        } >> "$AUDIT_FILE"
        ;;
    "Edit"|"MultiEdit"|"Write")
        FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
        {
            echo "### File Operation: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            echo "**Target File:** \`$FILE_PATH\`"
            echo
        } >> "$AUDIT_FILE"
        ;;
    "Read")
        FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
        {
            echo "### File Read: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            echo "**File:** \`$FILE_PATH\`"
            echo
        } >> "$AUDIT_FILE"
        ;;
    "Grep"|"Glob")
        PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // ""')
        {
            echo "### Search Operation: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            echo "**Pattern:** \`$PATTERN\`"
            echo
        } >> "$AUDIT_FILE"
        ;;
    "Task")
        DESCRIPTION=$(echo "$INPUT" | jq -r '.tool_input.description // ""')
        {
            echo "### Agent Launch: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            [ -n "$DESCRIPTION" ] && echo "**Description:** $DESCRIPTION"
            echo
        } >> "$AUDIT_FILE"
        ;;
    *)
        # Generic logging for other tools
        {
            echo "### Tool Usage: $TOOL_NAME"
            echo "**Timestamp:** $TIMESTAMP"
            echo
        } >> "$AUDIT_FILE"
        ;;
esac

# Always allow execution to continue
echo '{"continue": true}'

Understanding the Script

Key Components

  • get_current_audit_file - Checks for an active session file; creates default if none exists
  • ensure_audit_file - Creates the Markdown log file with a header if it's the first entry
  • INPUT=$(cat) - Reads the JSON data that Claude pipes via stdin
  • jq -r '.tool_name' - Parses JSON to extract the tool's name and input
  • echo '{"continue": true}' - Signals Claude that the tool execution can proceed

Step 3: Create Slash Commands

Slash commands make managing audit sessions easy. Create these in your project's .claude/commands/ directory.

The /audit Command

Create .claude/commands/audit.md:

---
description: Start a new audit file with an activity name/label
allowed-tools: Bash(date:*), Bash(echo:*), Bash(mkdir:*), Write
---

## Task
Create a new audit file for tracking all activities with the specified activity name.

### Parsed Arguments
Activity name: {{ARG1:activity_name}}

### Implementation

```bash
TIMESTAMP=$(date +"%Y_%m_%d-%H_%M")
ACTIVITY_NAME="{{activity_name}}"
SAFE_ACTIVITY=$(echo "$ACTIVITY_NAME" | tr ' ' '_' | tr -cd '[:alnum:]_-')
AUDIT_FILE="debugging/audit/audit_$TIMESTAMP-$SAFE_ACTIVITY.md"

mkdir -p debugging/audit
echo "$AUDIT_FILE" > ~/.claude/current_audit_file

cat > "$AUDIT_FILE" << EOF
# Audit Log

**Session Started:** $(date '+%Y-%m-%d %H:%M:%S')
**Activity:** $ACTIVITY_NAME
**User:** $(whoami)
**Working Directory:** $(pwd)

---

## Activity Log

EOF

echo "Audit file created: $AUDIT_FILE"
echo "Auditing is now active for: $ACTIVITY_NAME"
```

The /audit-archive Command

Create .claude/commands/audit-archive.md:

---
description: Archive all but the most current audit file
allowed-tools: Bash(ls:*), Bash(mv:*), Bash(mkdir:*)
---

## Task
Move all audit files except the most recent one to the archive directory.

### Implementation

```bash
mkdir -p debugging/audit/archive

AUDIT_FILES=($(ls -t debugging/audit/audit_*.md 2>/dev/null))

if [ ${#AUDIT_FILES[@]} -le 1 ]; then
    echo "Nothing to archive"
    exit 0
fi

CURRENT_FILE="${AUDIT_FILES[0]}"
echo "Keeping current: $(basename $CURRENT_FILE)"

for i in "${!AUDIT_FILES[@]}"; do
    if [ $i -eq 0 ]; then continue; fi
    FILE="${AUDIT_FILES[$i]}"
    mv "$FILE" "debugging/audit/archive/"
    echo "Archived: $(basename $FILE)"
done

echo "Archive complete"
```

Step 4: Configure the Hook

Add the hook configuration to your project's .claude/settings.local.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/audit_logger.sh"
          }
        ]
      }
    ]
  }
}

The "matcher": ".*" ensures the hook runs before every tool invocation.

Testing Your Audit System

  1. Start a Session: Run /audit test-run in Claude
  2. Run Commands: Execute a few shell commands like ls -la
  3. Check the Log: Open the new audit_*.md file in debugging/audit/
  4. Archive Logs: Run /audit-archive to tidy up old logs

Sample Log Output

# Audit Log

**Session Started:** 2025-09-26 22:10:15
**Activity:** test-run
**User:** developer
**Working Directory:** /home/developer/project

---

## Activity Log

### Tool Invocation: Bash
**Timestamp:** 2025-09-26 22:10:16

**Command:**
```bash
ls -la
```

### File Read: Read
**Timestamp:** 2025-09-26 22:10:18
**File:** `src/main.py`

Troubleshooting Common Issues

Script Not Executable

Your script must have execute permissions:

chmod +x ~/.claude/hooks/audit_logger.sh

jq Not Installed

The logger depends on jq for JSON parsing:

brew install jq  # macOS
apt install jq   # Ubuntu/Debian

Incorrect JSON in settings.local.json

Validate your JSON with a linter. Misplaced commas cause silent failures.

Enterprise Best Practices

For teams requiring SOX or SOC2 compliance:

Security

  • Sanitize inputs before logging
  • Redact sensitive data (API keys, passwords)
  • Use file permissions for log access control

Immutability

  • Set archived logs to read-only (chmod 444)
  • Use append-only storage if available
  • Hash logs for integrity verification

SIEM Integration

  • Send logs to Splunk/Datadog
  • Enable real-time alerting
  • Centralize multi-project logs

Retention

  • Define retention policies
  • Automate archival to cold storage
  • Maintain audit trail of deletions

Comprehensive Multi-Hook Example

For production use, you might want multiple hooks working together. Here's a complete configuration that logs pre-execution, post-execution, and session end:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/log_pre_tool.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/log_post_tool.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/log_stop.sh"
          }
        ]
      }
    ]
  }
}

Continue Your Journey

With your audit system in place, you've transformed Claude into a fully accountable development partner. Explore these related topics:

Complete Traceability

With this system in place, you've transformed your AI assistant into a reliable, enterprise-ready partner with full accountability. No more mystery operations. Just clear, undeniable proof of every action.