[BUG] Scheduled tasks run inside the user's working tree and leave orphaned git
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.lockand.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.lockwith 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):
- The scheduler should RECLAIM its own
.claude/scheduled_tasks.lockbefore
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.
- 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.
- 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:
- 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).
- Use that same working tree from one or more interactive Claude Code
sessions in
parallel (the normal multi-session setup).
- 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.
- Observe the leftovers in the working tree:
.git/index.lockand/or.git/HEAD.lockremain on disk..claude/scheduled_tasks.lockstill references the now-exited pid.
- 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_