[BUG] Scheduled tasks run inside the user's working tree and leave orphaned git

Open 💬 0 comments Opened Jun 22, 2026 by fzsigmond

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?

What happened (real incident, 2026-06-21 ~05:17):

  • The scheduled task made a commit and was interrupted mid git operation, leaving orphaned .git/index.lock and .git/HEAD.lock. This blocked ALL subsequent git in the shared working tree ("Unable to create .git/index.lock") until I removed them manually, hours later.
  • The scheduler's own mutex, .claude/scheduled_tasks.lock, was left pointing at a dead pid (the runner process had exited), silently blocking the scheduler with no notification.

What Should Happen?

Environment:

  • Claude Code installed locally on macOS.
  • I use scheduled tasks ("Cowork") for a daily routine that updates a status

file
in a git repo and commits/pushes it.

  • The same repository is used by several interactive Claude Code sessions in

parallel, in the SAME working tree.

What happened (real incident, 2026-06-21 ~05:17):

  • The scheduled task made a commit and was interrupted mid git operation,

leaving
orphaned .git/index.lock and .git/HEAD.lock. This blocked ALL subsequent
git
in the shared working tree ("Unable to create .git/index.lock") until I
removed
them manually, hours later.

  • The scheduler's own mutex, .claude/scheduled_tasks.lock, was left pointing

at a
dead pid (the runner process had exited), silently blocking the scheduler
with no
notification.

Root cause:
The scheduled task operates in the user's working tree, shared with
interactive
sessions. Git locks (.git/index.lock, .git/HEAD.lock) and the index are
per-working-tree resources; when the task crashes mid-operation it blocks
everyone
else. And the scheduler's mutex is not reclaimed when its owning pid dies.

Primary request (structural fix):
Run each scheduled task in a DEDICATED working tree (its own clone or a git
worktree), isolated from the user's working tree. This eliminates, in one
move:

  • contention on .git/index.lock / .git/HEAD.lock with interactive sessions
  • risk of index contamination (committing files staged by another session)
  • the scheduler mutex living in the user's tree (it would move to the

dedicated
clone; a task crash would never touch the working tree)
For tasks that only edit files and commit (i.e., don't build/run the app), a
dedicated clone is lightweight (no node_modules needed).

Secondary requests (if a dedicated working tree isn't feasible):

  1. The scheduler should RECLAIM its own .claude/scheduled_tasks.lock before

launching a task: if the owning pid is dead, or alive but with a start time that diverges from the procStart recorded in the lock (guards against pid reuse), remove it and proceed. macOS note: use ps -o lstart=ps -o etimes= does NOT exist on macOS.

  1. The task's git sequence should run as a single operation with guaranteed lock cleanup on failure/crash (a trap), never as separate, unguarded calls.
  2. Never use git add -A/./<dir> in a scheduled task on a shared tree (it would capture other sessions' WIP). Stage and commit explicitly per file (pathspec).

Mitigation I applied on my side (imperfect):
A SessionStart hook that sweeps stale git locks (>10min) and the scheduled_tasks.lock when the owning pid is dead / procStart diverges. But it only runs when I open a human session, not right before each scheduled run - which is why the proper fix belongs in the scheduler runtime.

Error Messages/Logs

Steps to Reproduce

Steps to reproduce:

  1. On macOS, configure a Claude Code scheduled task that, on a cron, edits a

tracked file in a local git repo and commits + pushes it. Point the task at
the
user's normal working tree (not a dedicated checkout).

  1. Use that same working tree from one or more interactive Claude Code

sessions in
parallel (the normal multi-session setup).

  1. Let the scheduled task fire and interrupt its process while it is mid git

operation — e.g. the runner process is killed, the machine sleeps, or the
task's
execution window ends between git add and the completion of git commit.

  1. Observe the leftovers in the working tree:
  • .git/index.lock and/or .git/HEAD.lock remain on disk.
  • .claude/scheduled_tasks.lock still references the now-exited pid.
  1. From any session, run any git command (e.g. git status → a commit):

→ fails with fatal: Unable to create '.git/index.lock': File exists.
The scheduler also does not launch the next run, because its mutex is held
by a
dead pid. Neither condition self-heals; a manual rm of the lock files is
required to recover.

Expected: an interrupted/crashed scheduled task must not leave locks that
block
unrelated interactive sessions, and the scheduler should reclaim its own stale
mutex (dead pid / pid reuse) before the next run.

Frequency: intermittent — triggers whenever a scheduled run is interrupted mid
git
op. In my case it surfaced once (2026-06-21) and the dead-pid mutex had
persisted
unnoticed for ~2 weeks.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.178 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗