DEV Community

Cover image for Don’t Let Claude Mess Up Your Code: Git Stash Checkpoint System for Claude Code
ztor2
ztor2

Posted on

Don’t Let Claude Mess Up Your Code: Git Stash Checkpoint System for Claude Code

Developers today are entering a new era with Claude Code.

  • Claude Code can edit multiple parts of your project at once to satisfy a complex query, while considering the whole codebase.
  • It’s a powerful feature, but it sometimes cause unexpected changes and the project ends up broken.
  • In this situation, users have no clear way to recover.
  • You could make commits during each task, but that clutters your Git history.
  • A better option is an automated checkpoint system using git stash and Claude’s Hooks.
  • What is 'Hooks'? - They're like event listeners for Claude Code, allowing you to run a command or script when certain events happen (like when Claude starts or stops).
  • Check out the official docs for more details: https://docs.claude.com/en/docs/claude-code/hooks
  • This setup creates a stash every time Claude Code finishes a single complete answer.
  • It keeps up to 10 checkpoints, dropping the oldest when the limit is reached.

1. Set up project structure

├── .claude/
│   ├── settings.local.json
│   └── logs/
├── ...
└── checkpoint.sh
Enter fullscreen mode Exit fullscreen mode

2. Configure settings.local.json file

  • Make sure to replace YOUR_PROJECT_DIR with the actual path to your project.
{
    "permissions": {
      "deny": [
      ]
    },
    "hooks": {
      "Stop": [
        {
          "hooks": [
            {
              "type": "command",
              "command": "YOUR_PROJECT_DIR/checkpoint.sh",
              "timeout": 30000
            }
          ]
        }
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

3. Set up checkpoint.sh file

  • You can customize the checkpoint rules(max number of stashes or logging behavior)
  • If you run into permission issues, you might need to run chmod +x checkpoint.sh to make the file executable.
#!/bin/bash
# Claude Code stop hook: checkpoint manager

# Log directory and file
LOG_DIR=".claude/logs"
LOG_FILE="$LOG_DIR/checkpoint.log"
mkdir -p "$LOG_DIR"

# Logging level via env vars
CHECKPOINT_DEBUG=${CHECKPOINT_DEBUG:-false}
CHECKPOINT_VERBOSE=${CHECKPOINT_VERBOSE:-true}

# Log rotation: >50KB, keep current + .old
rotate_logs() {
    if [ -f "$LOG_FILE" ] && [ $(wc -c < "$LOG_FILE" 2>/dev/null || echo 0) -gt 51200 ]; then
        # Remove old file, move current to .old
        [ -f "$LOG_FILE.old" ] && rm -f "$LOG_FILE.old"
        mv "$LOG_FILE" "$LOG_FILE.old"
    fi
}

# Logging helpers (conditional)
log_debug() {
    [ "$CHECKPOINT_DEBUG" = "true" ] && echo "[$(date '+%H:%M:%S')] DEBUG: $1" >> "$LOG_FILE"
}

log_info() {
    [ "$CHECKPOINT_VERBOSE" = "true" ] && echo "[$(date '+%H:%M:%S')] INFO: $1" >> "$LOG_FILE"
}

log_error() {
    echo "[$(date '+%H:%M:%S')] ERROR: $1" >> "$LOG_FILE"
}

# Rotate logs
rotate_logs

# Begin debug
log_debug "Stop hook started with args: $*"

# Read Claude Code JSON input
HOOK_DATA=""
if [ -t 0 ]; then
    log_debug "No stdin data detected (terminal mode)"
else
    log_debug "Reading JSON data from stdin"
    HOOK_DATA=$(cat)
    log_debug "Received JSON: $HOOK_DATA"
fi

# Guard: stop_hook_active true => exit (loop prevention)
if echo "$HOOK_DATA" | grep -q '"stop_hook_active":\s*true'; then
    log_info "Stop hook already active, exiting to prevent loop"
    exit 0
fi

# Verify git repo
if ! git rev-parse --git-dir >/dev/null 2>&1; then
    log_debug "Not a git repository, exiting"
    exit 1
fi

# Check working tree changes
GIT_STATUS=$(git status --porcelain 2>/dev/null)
if [ -z "$GIT_STATUS" ]; then
    log_debug "No changes detected, exiting"
    exit 0
fi

log_info "Changes detected: $(echo "$GIT_STATUS" | wc -l) files"

# Build stash message (arg or git status)
if [ -n "$1" ]; then
    MESSAGE="$1"
    log_debug "Using command line message: $MESSAGE"
else
    # From status, take filenames sorted by mtime (max 3)
    FILES_WITH_TIME=""
    while IFS= read -r line; do
        if [ -n "$line" ]; then
            FILE_PATH=$(echo "$line" | cut -c4-)
            if [ -f "$FILE_PATH" ]; then
                # Store with mtime in seconds
                MOD_TIME=$(stat -f "%m" "$FILE_PATH" 2>/dev/null || echo "0")
                FILES_WITH_TIME="$FILES_WITH_TIME$MOD_TIME:$FILE_PATH\n"
            fi
        fi
    done <<< "$GIT_STATUS"

    # Sort by mtime desc, keep names
    SORTED_FILES=$(echo -e "$FILES_WITH_TIME" | grep -v "^$" | sort -rn -t: -k1 | cut -d: -f2- | head -3)

    # Compose message
    CHANGED_FILES=$(echo "$SORTED_FILES" | tr '\n' ' ' | sed 's/ $//')
    TOTAL_COUNT=$(echo "$GIT_STATUS" | wc -l)
    if [ $TOTAL_COUNT -gt 3 ]; then
        MESSAGE="$CHANGED_FILES (and $(($TOTAL_COUNT - 3)) more)"
    else
        MESSAGE="$CHANGED_FILES"
    fi
    log_debug "Generated message from git status (sorted by modification time): $MESSAGE"
fi

# Make checkpoint label
TIMESTAMP=$(date '+%y%m%d:%H:%M:%S')
STASH_MESSAGE="agent: $TIMESTAMP $MESSAGE"

log_info "Creating checkpoint: $STASH_MESSAGE"

# Run git stash
if git stash push --include-untracked -m "$STASH_MESSAGE" >/dev/null 2>&1; then
    log_info "Stash created successfully"

    # Reapply stash
    if git stash apply stash@{0} >/dev/null 2>&1; then
        log_info "Stash reapplied successfully"
    else
        log_debug "Warning: Failed to reapply stash"
    fi
else
    log_debug "Error: Failed to create stash"
    exit 1
fi

# Agent stash lifecycle (max 10)
AGENT_STASH_COUNT=$(git stash list 2>/dev/null | grep "agent:" | wc -l)
log_debug "Current agent stash count: $AGENT_STASH_COUNT"

while [ $AGENT_STASH_COUNT -gt 10 ]; do
    OLDEST_AGENT_STASH=$(git stash list 2>/dev/null | grep "agent:" | tail -1 | cut -d: -f1)
    if [ -n "$OLDEST_AGENT_STASH" ]; then
        log_info "Removing oldest agent stash: $OLDEST_AGENT_STASH"
        git stash drop "$OLDEST_AGENT_STASH" >/dev/null 2>&1
        AGENT_STASH_COUNT=$((AGENT_STASH_COUNT - 1))
    else
        break
    fi
done

# Success message
FINAL_MESSAGE="Checkpoint saved: stash@{0} - $STASH_MESSAGE"
echo "$FINAL_MESSAGE"
log_info "$FINAL_MESSAGE"
Enter fullscreen mode Exit fullscreen mode

4. Restore from a stash

View checkpoints:

git stash list
Enter fullscreen mode Exit fullscreen mode

Example:

Restore one:

git stash apply stash@{0}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)