Skip to content

Tiwas/claude-code-file-locks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-code-file-locks

Cooperative FIFO reader/writer file locks for Claude Code, implemented entirely as user-level hooks. When several Claude Code sessions/agents run against the same working tree, a Read waits while another session is mid-Write, and an Edit/Write/MultiEdit/NotebookEdit waits while another session is mid-Read or Write — so concurrent agents don't clobber each other's file operations.

  • Reader/writer semantics — many concurrent readers, exclusive writers.
  • FIFO fairness — no writer starvation, no reader starvation.
  • Multi-file locks — lock several files atomically (all-or-nothing).
  • Self-healing — leases expire, stale locks are garbage-collected; a crashed session never wedges the lock.
  • Git-safe — never touches anything under .git/ (Git keeps its own locking), and lock artifacts use a distinct .agentlock suffix so they never collide with real dependency lockfiles (poetry.lock, Cargo.lock, …).
  • Fail-open — it's an advisory cooperative lock, not a security boundary; any bug in the lock code lets the operation through rather than blocking the agent.

No daemon, no database, no dependencies beyond Python 3.10+. State lives in tiny sidecar JSON files next to the locked files.


Compatibility

Tested and working well under both Claude Code and Codex. The lock engine (file_lock_core.py) is agent-agnostic — it reads a tool payload (JSON, with the target file path and the pre/post event) on stdin and manages the .agentlock sidecars — so it runs under any agent that can invoke a command on its pre-tool and post-tool lifecycle. Only the thin glue is host-specific: the event names and the hookSpecificOutput permission schema are Claude Code's. The bundled install.sh wires Claude Code; under Codex, point its equivalent pre/post-tool hook at the very same read_file_lock.py / write_file_lock.py scripts.


Install

git clone https://github.com/Tiwas/claude-code-file-locks.git
cd claude-code-file-locks
./install.sh

install.sh is idempotent. It:

  1. copies the three hook scripts into ~/.claude/hooks/,
  2. merges the hook wiring into ~/.claude/settings.json (preserving any existing settings and hooks; re-running never duplicates entries),
  3. appends *.agentlock and *.agentlock.mutex to your global git excludes file.

Then start a new Claude Code session (hooks load at session start).

Uninstall with ./uninstall.sh.

Let your agent install it (straight from this repo)

Paste this to Claude Code or Codex and it will clone + install everything itself:

Install cooperative file locking from https://github.com/Tiwas/claude-code-file-locks :
1. Clone it into a scratch dir (or `git pull` if you already have it).
2. Read its README.md.
3. If you are Claude Code, run `./install.sh`. If you are Codex, follow the README's
   "Compatibility" note and wire hooks/read_file_lock.py + hooks/write_file_lock.py
   into Codex's pre-tool and post-tool hooks (acquire on pre, release on post).
4. Run `./tests/smoke_test.sh` and confirm it prints "ALL SMOKE TESTS PASSED".
5. Then tell me to restart this session so the hooks load.
Do not commit any .agentlock files; the installer adds them to the global gitignore.

Prefer a single self-contained prompt that writes every file from scratch (no clone)? See docs/AGENT_PROMPT.md.

Verify

./tests/smoke_test.sh

How it works

Each locked file X gets two sidecars:

File Purpose
X.agentlock JSON lock state: {version, target, active[], queue[]}
X.agentlock.mutex a short-lived mkdir mutex, held only while X.agentlock is rewritten

The hooks fire on Claude Code's tool lifecycle:

  • PreToolUseacquire. The hook process blocks (waits in FIFO order) until the lock is granted, then returns allow. That blocking is the lock — the tool cannot run until the hook returns.
  • PostToolUserelease.

A Read requests a read lease; Edit/Write/MultiEdit/NotebookEdit request a write lease.

Grant rules

  • Write is granted only when there are no active leases and the request is at the head of the FIFO queue (exclusive).
  • Read is granted when no writer is active and no writer sits ahead of it in the queue (so multiple readers share, but a queued writer is not starved).

FIFO order is a monotonic ticket (time.time_ns). Multi-file requests grab every per-file mutex in sorted path order (deadlock-free) and are granted only when all requested files can be granted at once.

Self-healing

  • Every lease carries an expires_at. Expired entries are dropped whenever the state is read, so a crashed/killed session is cleaned up automatically (default leases: 120 s for reads, 300 s for writes — far longer than a normal tool call, short enough to recover quickly).
  • The .agentlock file is deleted once it has no active or queued holders.
  • A .mutex left behind by a hard crash is force-removed after 30 s.

Atomic writes

The lock state is written to a temp file, fsync'd, then os.replace'd into place, so a reader never sees a half-written lock file.


Configuration

All optional, via environment variables:

Variable Default Meaning
CLAUDE_FILE_READ_LOCK_SECONDS 120 read-lease TTL (seconds)
CLAUDE_FILE_WRITE_LOCK_SECONDS 300 write-lease TTL (seconds)
CLAUDE_FILE_LOCK_WAIT_SECONDS 540 max time to wait for a lock before denying
CLAUDE_FILE_LOCK_POLL_SECONDS 0.1 poll interval while waiting

Keep the hook timeout in settings.json (default 600) CLAUDE_FILE_LOCK_WAIT_SECONDS, or Claude Code will kill the hook before it can finish waiting.


What gets wired into settings.json

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Read", "hooks": [{ "type": "command", "command": "python3 \"$HOME/.claude/hooks/read_file_lock.py\"", "timeout": 600 }] },
      { "matcher": "Edit|Write|MultiEdit|NotebookEdit", "hooks": [{ "type": "command", "command": "python3 \"$HOME/.claude/hooks/write_file_lock.py\"", "timeout": 600 }] }
    ],
    "PostToolUse": [
      { "matcher": "Read", "hooks": [{ "type": "command", "command": "python3 \"$HOME/.claude/hooks/read_file_lock.py\"", "timeout": 600 }] },
      { "matcher": "Edit|Write|MultiEdit|NotebookEdit", "hooks": [{ "type": "command", "command": "python3 \"$HOME/.claude/hooks/write_file_lock.py\"", "timeout": 600 }] }
    ]
  }
}

(The installer uses the absolute hook directory rather than $HOME so it works regardless of how the hook command is evaluated.)


Semantics & limitations (read this)

  • Advisory & cooperative. Every participant must run these hooks. It does not stop a non-Claude process — or a session without the hooks — from touching files.
  • Lock scope = one tool call. The lease is held between PreToolUse and PostToolUse, i.e. for the duration of a single Read/Edit/Write. It does not make a multi-step read → think → edit sequence atomic across agents; another agent can write between your read and your edit. (Hooks are separate processes per tool call, so a lease can't span the agent's reasoning.)
  • Fail-open by design. If the lock code raises, the operation is allowed (with a stderr note). A locking bug must never brick file access.
  • Why a distinct PreToolUse schema? The decision is emitted as {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow|deny", ...}}. A top-level {"permissionDecision": …} is silently ignored by Claude Code, which would make deny-on-timeout a no-op.

Requirements

  • Python 3.10+ (uses X | None syntax at runtime).
  • A POSIX-ish filesystem (uses mkdir/os.replace/fsync for atomicity).
  • Git ≥ 2.x for the global-excludes step (optional).

License

MIT — see LICENSE.

About

Cooperative FIFO read/write file locks for concurrent Claude Code (and Codex) sessions, via hooks — advisory, multi-file, self-healing, git-safe.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors