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 in meta.json to 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.go relies 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.md to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →