[BUG] Stale history.jsonl.lock directory causes indefinite freeze (no PID-based recovery)

Resolved 💬 7 comments Opened Jan 31, 2026 by clthuang Closed Mar 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code uses a directory-based lock (~/.claude/history.jsonl.lock) to synchronize writes to history.jsonl. However, if this lock directory exists from a previous crashed/killed session or from copying/restoring a .claude backup, Claude Code freezes indefinitely on startup.

The lock mechanism has no stale lock detection — it doesn't check if the process that created the lock is still alive.

Root Cause

The locking mechanism:

  1. Acquire: mkdir history.jsonl.lock (atomic, fails if exists)
  2. Write: append to history.jsonl
  3. Release: rmdir history.jsonl.lock

If step 3 never runs (crash, kill -9, Ctrl+C during startup, or copying .claude directory), the lock directory remains. On next startup, Claude sees the directory exists, assumes another process holds the lock, and waits forever.

Reproduction Steps

Method 1: Backup/restore

  1. Copy ~/.claude to ~/.claude_backup while Claude is running (or after a crash)
  2. Restore: cp -R ~/.claude_backup/* ~/.claude/
  3. Start Claude Code
  4. Result: Freeze on first query (before any output)

Method 2: Simulate crash

  1. Start Claude Code
  2. Immediately kill -9 <pid> during first prompt submission
  3. Restart Claude Code
  4. Result: Freeze (lock directory remains from killed process)

What Should Happen?

Claude Code should detect stale locks and recover automatically:

  1. Store PID in lock: Write the owning process PID to a file inside the lock directory
  2. Check PID on conflict: If lock exists, check if the PID is still alive (kill -0 <pid>)
  3. Auto-cleanup: If PID is dead, remove the stale lock and proceed
  4. Timeout: After N seconds of waiting, warn user and offer to force-acquire

Error Messages/Logs

No error message — silent freeze. The process appears to hang waiting for lock acquisition.

Workaround

# Remove stale lock manually
rmdir ~/.claude/history.jsonl.lock

# Or add to shell profile as safety alias
alias claude='rmdir ~/.claude/history.jsonl.lock 2>/dev/null; command claude'

Environment

  • OS: macOS (Darwin 25.2.0), but likely affects all platforms
  • Claude Code Version: Latest
  • Shell: zsh

Related Issues

  • #15739 - Windows race condition with history.jsonl.lock (different bug, same file)
  • #22057 - Mentions stale lock as contributing factor to CPU loop

Additional Context

The history.jsonl.lock in my backup was an empty directory — the lock mechanism uses directory existence as the lock signal, with no contents to identify the owning process. This makes stale lock detection impossible without external tracking.

View original on GitHub ↗

This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗