Understanding loop-session.lock.d vs Regular Session Locking in Claude-Code-Harness
The loop-session.lock.d directory-based lock stores session metadata in a meta.json file to enable precise idempotency guards and selective WIP warning suppression, while the legacy loop-session.lock file acts as a simple binary flag that blocks all subsequent invocations regardless of session ownership.
Claude-Code-Harness employs two distinct locking mechanisms to prevent concurrent execution of long-running harness-loop sessions. Understanding the difference between the modern directory-based loop-session.lock.d approach and the legacy file-based loop-session.lock method is crucial for implementing proper session management and avoiding race conditions in CI/CD pipelines.
Directory-Based Locking: The loop-session.lock.d Approach
The current implementation uses a directory structure located at ./.claude/state/locks/loop-session.lock.d/ to manage exclusive access to harness-loop sessions. This approach provides rich session context through embedded metadata.
Structure and Metadata Storage
Unlike simple file locks, the directory-based mechanism contains a meta.json file that stores the active session identifier. According to the flow documentation in skills/harness-loop/references/flow.md (line 49), this structure enables the harness to distinguish between different session owners.
The metadata payload typically includes:
{
"session_id": "2026-05-28-01-abcdef"
}
This JSON structure allows the PreCompact hook and Go implementation to perform intelligent session comparison rather than merely checking for file existence.
Creation and Detection Logic
To acquire the directory-based lock, the harness creates the directory structure and writes the session metadata:
# Acquire lock at loop start
LOCK_DIR=".claude/state/locks/loop-session.lock.d"
mkdir -p "$LOCK_DIR"
# Store session metadata
echo '{"session_id":"2026-05-28-01-abcdef"}' > "$LOCK_DIR/meta.json"
Detection combines directory existence checks with session ID validation:
# Verify lock ownership
if [ -d "$LOCK_DIR" ]; then
LOCK_SESSION=$(jq -r '.session_id' "$LOCK_DIR/meta.json")
if [ "$LOCK_SESSION" = "$CURRENT_SESSION_ID" ]; then
# Same session - suppress WIP warnings and exit early
exit 0
fi
fi
Implementation in PreCompact Hooks
The Go implementation in go/cmd/harness/pre_compact.go (line 187) reads the meta.json file to determine whether to emit WIP warnings or return early. This logic enables selective suppression of pre-compact warnings when the lock belongs to the active loop, a feature impossible with the legacy file-based approach.
File-Based Locking: The Legacy loop-session.lock Method
The original implementation used a plain file at ./.claude/state/locks/loop-session.lock as a simple mutex. This mechanism persists primarily in test suites but lacks the sophistication required for modern session management.
Legacy Implementation Characteristics
The file-based lock functions as a binary semaphore:
# Legacy lock acquisition
LOCK_FILE=".claude/state/locks/loop-session.lock"
mkdir -p "$(dirname "$LOCK_FILE")"
> "$LOCK_FILE" # Creates empty file
Since this file contains no embedded session identifier—sometimes only an empty JSON object or no content at all—any existing lock blocks all subsequent invocations, even those originating from the same session.
Current Usage in Tests
The legacy lock remains relevant in tests/test-harness-loop-guard.sh (line 11), where it simulates the original exclusive-run guard. This test script validates basic mutual exclusion but does not exercise the session-aware logic required for production harness-loop flows.
Key Differences Compared
| Feature | loop-session.lock.d (Directory) |
loop-session.lock (File) |
|---|---|---|
| Storage | Directory containing meta.json |
Plain file (often empty) |
| Session Tracking | Stores session_id for ownership validation |
No session metadata |
| Idempotency | Recognizes same-session re-entry | Blocks all invocations uniformly |
| Use Case | Production harness-loop sessions | Legacy tests and early versions |
| Location | .claude/state/locks/loop-session.lock.d/ |
.claude/state/locks/loop-session.lock |
The directory-based approach enables precise idempotency guards and selective WIP warning suppression, while the file-based approach provides only a simple "is someone running?" flag without ownership context.
Practical Implementation Examples
Acquiring the Directory-Based Lock
Production code should implement the directory-based pattern as defined in the harness-loop flow:
#!/bin/bash
set -euo pipefail
LOCK_DIR=".claude/state/locks/loop-session.lock.d"
SESSION_ID="${HARNESS_SESSION_ID:-$(date +%Y%m%d-%H%M%S)}"
# Create directory lock
mkdir -p "$LOCK_DIR"
# Write metadata
cat > "$LOCK_DIR/meta.json" <<EOF
{
"session_id": "${SESSION_ID}"
}
EOF
echo "Lock acquired for session: $SESSION_ID"
Checking Session Ownership
Hooks and pre-compact checks should validate session continuity, as implemented in go/cmd/harness/pre_compact.go:
check_session_ownership() {
local current_session="$1"
local lock_dir=".claude/state/locks/loop-session.lock.d"
if [ ! -d "$lock_dir" ]; then
return 1 # No lock exists
fi
local locked_session
locked_session=$(jq -r '.session_id' "$lock_dir/meta.json" 2>/dev/null || echo "")
if [ "$locked_session" = "$current_session" ]; then
return 0 # Same session
else
return 1 # Different session
fi
}
Legacy File Lock Pattern
For reference, the legacy pattern used in tests/test-harness-loop-guard.sh appears as:
LOCK_FILE="${PLUGIN_ROOT}/.claude/state/locks/loop-session.lock"
# Simple existence check
if [ -f "$LOCK_FILE" ]; then
echo "Another loop is already running" >&2
exit 1
fi
# Create lock
> "$LOCK_FILE"
Cleaning Up Locks
Regardless of the locking mechanism, proper cleanup prevents orphaned locks:
# Cleanup for directory-based locks (production)
rm -rf ".claude/state/locks/loop-session.lock.d"
# Cleanup for legacy file locks (tests)
rm -f ".claude/state/locks/loop-session.lock"
Integration tests in tests/integration/loop-max-cycles.sh (line 14) demonstrate proper teardown of the directory-based lock after successful harness-loop completion.
Summary
- Directory-based locking (
loop-session.lock.d) is the current production standard, storing session metadata inmeta.jsonto enable intelligent session validation and WIP warning suppression. - File-based locking (
loop-session.lock) is a legacy mechanism that provides basic mutual exclusion but cannot distinguish between different session owners. - The Go implementation in
pre_compact.gorelies on the directory structure to make idempotency decisions during pre-compact hooks. - Production implementations should always use the directory-based approach defined in
skills/harness-loop/references/flow.mdto support long-running harness sessions properly.
Frequently Asked Questions
Why does claude-code-harness use a directory instead of a simple lock file?
The directory structure with an embedded meta.json file allows the harness to store session-specific metadata, including the session_id. This enables idempotent session handling, where the same harness-loop session can re-enter without triggering false-positive "already running" errors or redundant WIP warnings. According to the design documentation in docs/long-running-harness.md (section 4-2), this approach supports future extensibility for adding timestamps and owner PIDs.
Can I safely remove the legacy loop-session.lock file from my codebase?
Yes. The plain file lock at .claude/state/locks/loop-session.lock is only referenced in historic test suites like tests/test-harness-loop-guard.sh. Production code should rely exclusively on the directory-based mechanism. Removing legacy lock file references reduces confusion and ensures consistent behavior with the current Go implementation in go/cmd/harness/pre_compact.go.
How does the PreCompact hook determine whether to suppress WIP warnings?
The PreCompact hook reads .claude/state/locks/loop-session.lock.d/meta.json and compares the stored session_id against the incoming hook's session identifier. If they match, the hook recognizes this as a continuation of the same long-running session and suppresses redundant WIP warnings. This logic, implemented at line 187 of pre_compact.go, is impossible with the legacy file-based lock because it lacks session context.
What happens if a harness-loop crashes without cleaning up the lock directory?
Since the lock persists as a directory in .claude/state/locks/loop-session.lock.d/, subsequent sessions will detect the existing lock and check the session_id. If the crashed session cannot be resumed, manual intervention or a timeout-based cleanup mechanism would be required to remove the orphaned lock. Unlike the legacy file lock, the metadata in meta.json allows operators to identify which specific session left the lock behind.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →