Real-time tracking of AI code edits using Claude Code's PostToolUse hooks.
What it tracks: Lines of Codes modified by Claude Code. Including: File path, tool type, timestamp, git context (branch, commit, repo, author), diff stats (lines added/removed/changed), session and tool use IDs.
What it doesn't track: It does not record the lines of code actually merged into your code base. However, it does minimize impact to developers current workflows.
How it works: By tapping into Claude Code's PostToolUse Hook it posts the metadata to an http endpoint specified by the CLAUDE_EDITS_ENDPOINT environment variable.
NOTE: Experimental example code. Not reviewed for production use. YMMV.
1. Copy the script (see full script at bottom) to .claude/hooks/capture-edits.sh and make it executable:
chmod +x .claude/hooks/capture-edits.sh2. Add to your .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/capture-edits.sh"
}
]
}
]
}
}3. Configure environment variables in ~/.bashrc or ~/.zshrc:
# HTTP endpoint (default: postbin test URL)
export CLAUDE_EDITS_ENDPOINT=https://your-analytics.com/api/edits
# Enable local logging (default: false)
export CLAUDE_EDITS_LOG_LOCAL=true- Git repository, Claude Code,
jq,curl
{
"event": "ai_edit",
"timestamp": "2026-04-03T18:30:45Z",
"tool": "Edit",
"file_path": "/absolute/path/to/file.ts",
"session_id": "uuid",
"tool_use_id": "toolu_01ABC123",
"git_branch": "feature/my-feature",
"git_commit": "abc123...",
"git_repo": "https://gitlab.com/org/repo.git",
"git_author_email": "user@example.com",
"lines_added": 7,
"lines_removed": 2,
"lines_changed": 5
}# Verify hook is configured
jq '.hooks.PostToolUse' .claude/settings.json
# View HTTP errors
tail .git/claude-ai/edits-errors.log
# View local edits (if enabled)
tail .git/claude-ai/edits.jsonl | jq .#!/usr/bin/env bash
set -euo pipefail
# Read hook payload from stdin
PAYLOAD=$(cat)
# Extract fields using jq
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name')
FILE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path')
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id')
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id')
CWD=$(echo "$PAYLOAD" | jq -r '.cwd')
# Get git context (with fallbacks)
GIT_BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
GIT_COMMIT=$(git -C "$CWD" rev-parse HEAD 2>/dev/null || echo "unknown")
REPO_URL=$(git -C "$CWD" config --get remote.origin.url 2>/dev/null || echo "unknown")
GIT_AUTHOR_EMAIL=$(git -C "$CWD" config --get user.email 2>/dev/null || echo "unknown")
# Calculate change statistics
if [ "$TOOL_NAME" = "Edit" ]; then
OLD_STRING=$(echo "$PAYLOAD" | jq -r '.tool_input.old_string')
NEW_STRING=$(echo "$PAYLOAD" | jq -r '.tool_input.new_string')
OLD_LINES=$(echo "$OLD_STRING" | wc -l | tr -d ' ')
NEW_LINES=$(echo "$NEW_STRING" | wc -l | tr -d ' ')
LINES_ADDED=$NEW_LINES
LINES_REMOVED=$OLD_LINES
LINES_CHANGED=$((NEW_LINES - OLD_LINES))
else
# Write tool - use git diff if file is tracked
if [ -f "$FILE_PATH" ] && git -C "$CWD" ls-files --error-unmatch "$FILE_PATH" 2>/dev/null; then
DIFF_STATS=$(git -C "$CWD" diff --numstat HEAD "$FILE_PATH" 2>/dev/null | head -1)
if [ -n "$DIFF_STATS" ]; then
LINES_ADDED=$(echo "$DIFF_STATS" | awk '{print $1}')
LINES_REMOVED=$(echo "$DIFF_STATS" | awk '{print $2}')
# Handle case where git diff returns "-" for binary files
[ "$LINES_ADDED" = "-" ] && LINES_ADDED=0
[ "$LINES_REMOVED" = "-" ] && LINES_REMOVED=0
else
LINES_ADDED=0
LINES_REMOVED=0
fi
else
# New file, count total lines
LINES_ADDED=$(wc -l < "$FILE_PATH" 2>/dev/null || echo 0)
LINES_REMOVED=0
fi
LINES_CHANGED=$((LINES_ADDED - LINES_REMOVED))
fi
# Build HTTP payload (compact JSON for JSONL format, flattened structure)
HTTP_PAYLOAD=$(jq -nc \
--arg event "ai_edit" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg tool "$TOOL_NAME" \
--arg file "$FILE_PATH" \
--arg session "$SESSION_ID" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg git_branch "$GIT_BRANCH" \
--arg git_commit "$GIT_COMMIT" \
--arg git_repo "$REPO_URL" \
--arg git_author_email "$GIT_AUTHOR_EMAIL" \
--argjson lines_added "$LINES_ADDED" \
--argjson lines_removed "$LINES_REMOVED" \
--argjson lines_changed "$LINES_CHANGED" \
'{
event: $event,
timestamp: $timestamp,
tool: $tool,
file_path: $file,
session_id: $session,
tool_use_id: $tool_use_id,
git_branch: $git_branch,
git_commit: $git_commit,
git_repo: $git_repo,
git_author_email: $git_author_email,
lines_added: $lines_added,
lines_removed: $lines_removed,
lines_changed: $lines_changed
}')
# Get endpoint from env var or use default
ENDPOINT="${CLAUDE_EDITS_ENDPOINT:-https://www.postb.in/1775266913389-6062681842595}"
# Check if local logging is enabled (default: false)
LOG_LOCAL="${CLAUDE_EDITS_LOG_LOCAL:-false}"
# Ensure state directory exists (needed for error log)
REPO_ROOT=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || echo "$CWD")
STATE_DIR="$REPO_ROOT/.git/claude-ai"
mkdir -p "$STATE_DIR"
ERROR_LOG="$STATE_DIR/edits-errors.log"
LOG_FILE="$STATE_DIR/edits.jsonl"
# Send to HTTP endpoint (non-blocking, with timeout)
# Background execution to prevent blocking Claude Code
(
curl -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-d "$HTTP_PAYLOAD" \
--max-time 5 \
--silent \
--fail \
2>> "$ERROR_LOG" || true
) &
# Log locally if enabled
if [ "$LOG_LOCAL" = "true" ]; then
echo "$HTTP_PAYLOAD" >> "$LOG_FILE"
fi
# Always exit successfully (never block Claude Code)
exit 0