CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-04-25Intermediate

When Claude Code Hits `index.lock` in VMs and CI: Three Workarounds That Actually Stick

When Claude Code freezes on `fatal: Unable to create '.git/index.lock'` inside a VM, container, or CI sandbox, here are three battle-tested workarounds — pick the one that matches the layer of failure you're actually hitting.

claude-code129git16troubleshooting87vmci2lock

You asked Claude Code to add, commit, and push a small change. Instead of finishing, it stopped on fatal: Unable to create '.git/index.lock': File exists. and refused to move. If you only see this in VMs, containers, or CI sandboxes — but never on your laptop — you're not alone. I hit this constantly when I started running Claude Code inside scheduled tasks, and the official "just delete the lock file" advice almost never holds for more than a day.

The reason this is hard to chase down is that the error message points at one symptom while the actual cause is usually three layers stacked on top of each other: ownership mismatch, leftover state from a previous bash session, and slow file system flushes. Treat only one and the next run will fail in the same place.

After enough scheduled-task crashes I gave up trying to "fix" the lock and switched to a layered set of workarounds that match the environment I was running in. The three patterns below are the ones that have survived months of nightly automation for me.

Why the same git command works locally but freezes inside a VM

It's tempting to blame Claude Code, but the CLI is just shelling out to git. The lock collision lives in the environment. Three things tend to go wrong inside a sandbox that simply do not happen on your laptop.

  • Ownership drift. When the VM creates /tmp (or your working directory) under a special user like nobody:nogroup, Claude Code's bash sessions — usually running as root or a regular user — can't write into it cleanly. git add partially succeeds, leaves a half-built index, and the lock survives.
  • Zombie state from prior bash calls. Each Claude Code bash invocation runs in its own process. If a previous git commit was killed mid-flight (timeout, SIGTERM, container reaping), the lock file persists with no parent process to clean it up.
  • File system flush latency. Shadow mounts, NFS-style volumes, and overlay file systems used by sandboxes don't always make a rm immediately visible to the next process. The lock looks gone, then git sees it again half a second later.

The "zombie state" version is by far the most common in environments like Cowork mode, where every bash call boots a fresh shell and the previous one's processes are unreachable. You can't kill what you don't have a PID for, so removing the lock is your only real option.

Workaround 1: Sweep the lock files and reset before every commit

If the failure is sporadic — once a week or so — the smallest fix is to wrap every commit with a deterministic cleanup. Three lines before git add will solve most cases.

# 1. Remove any lingering lock files (index, HEAD, refs)
find .git -maxdepth 2 -name "*.lock" -type f -delete 2>/dev/null
 
# 2. Force the index back into a known state
git reset --mixed HEAD 2>/dev/null
 
# 3. Wait for the file system to flush (NFS / shadow mount safety)
sync
sleep 1
 
# 4. Commit normally
git add content/
git commit -m "Add: new article"

The find is doing real work here. index.lock is the most famous one, but HEAD.lock and refs/heads/main.lock show up just as often, and missing either of those will hang the next command. The -maxdepth 2 keeps you from accidentally deleting valid lock files inside .git/objects/ during a pack operation.

This pattern is the right starting point if you want to keep your local script and your sandbox script identical. It's a small wedge that handles the everyday case without restructuring anything.

Workaround 2: Move the working directory to a writable home

If layer 1 isn't enough — you delete the lock today and tomorrow it's stuck again on the very first git add — the issue is almost certainly ownership on the parent directory. VMs that recycle /tmp between boots sometimes recreate /tmp/repos under a user you can't write to.

I solved this for good by adding a writability probe before cloning. If /tmp/repos is hostile that day, the script silently moves to $HOME/repos instead.

WORK="/tmp/repos/myproject"
 
# Probe: fall back to $HOME/repos if /tmp/repos is read-only
mkdir -p "$(dirname "$WORK")" 2>/dev/null
if ! touch "$(dirname "$WORK")/.write_test" 2>/dev/null; then
  echo "⚠️ /tmp/repos is not writable — falling back to $HOME/repos"
  WORK="$HOME/repos/myproject"
  mkdir -p "$HOME/repos"
fi
rm -f "$(dirname "$WORK")/.write_test" 2>/dev/null
 
# Use $WORK from here on
git clone --depth 1 "$REPO_URL" "$WORK"
cd "$WORK"

What this gives you is a script that auto-corrects on the days the sandbox state is bad, without any manual intervention. I run this in combination with the patterns from Fixing Claude Code Bash tool execution errors, which catches the related "command silently dies" failure mode. Together they removed almost all of the "what happened overnight?" debugging time from my scheduled tasks.

Workaround 3: Skip local git entirely and push via the GitHub REST API

For jobs where I cannot afford another failed run — daily content pushes that other systems depend on — I stopped fighting .git/index.lock and switched to the GitHub Git Data API. The code is longer, but lock collisions become structurally impossible: there is no .git directory to lock.

# Required env: GITHUB_TOKEN, OWNER, REPO, BRANCH (defaults to main)
TOKEN="$GITHUB_TOKEN"
OWNER="masakihirokawa"
REPO="myproject"
BRANCH="main"
API="https://api.github.com/repos/${OWNER}/${REPO}"
 
# 1. Upload the file as a base64 blob
CONTENT=$(base64 -w0 < content/articles/en/sample.mdx)
BLOB_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
  -d "{\"content\":\"${CONTENT}\",\"encoding\":\"base64\"}" \
  "${API}/git/blobs" | jq -r .sha)
 
# 2. Get the branch tip and its tree
PARENT_SHA=$(curl -sS -H "Authorization: token $TOKEN" \
  "${API}/git/refs/heads/${BRANCH}" | jq -r .object.sha)
BASE_TREE=$(curl -sS -H "Authorization: token $TOKEN" \
  "${API}/git/commits/${PARENT_SHA}" | jq -r .tree.sha)
 
# 3. Build a new tree with the new file layered on top
TREE_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
  -d "{\"base_tree\":\"${BASE_TREE}\",\"tree\":[{\"path\":\"content/articles/en/sample.mdx\",\"mode\":\"100644\",\"type\":\"blob\",\"sha\":\"${BLOB_SHA}\"}]}" \
  "${API}/git/trees" | jq -r .sha)
 
# 4. Create the commit and advance the ref
COMMIT_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
  -d "{\"message\":\"Add sample\",\"tree\":\"${TREE_SHA}\",\"parents\":[\"${PARENT_SHA}\"]}" \
  "${API}/git/commits" | jq -r .sha)
curl -sS -X PATCH -H "Authorization: token $TOKEN" \
  -d "{\"sha\":\"${COMMIT_SHA}\"}" \
  "${API}/git/refs/heads/${BRANCH}"

Those four steps — Blobs → Trees → Commits → Refs — are the canonical Git Data API flow. For a single file the whole sequence finishes in under five seconds and never touches the file system, so ownership and flush latency are out of the picture.

I would not recommend rewriting your day-to-day workflow this way. But for jobs that absolutely need to ship — a daily content push, a release commit, a status update — this pattern is worth the extra lines. If you want to deepen the surrounding design, Claude Code git push troubleshooting, end to end and Resilience patterns for long-running Claude Code automation make a good follow-up read.

How to choose between the three

Each workaround trades effort for breadth, and that trade-off is what should drive the decision.

  • Sporadic failures (once a week or so): stay with workaround 1 — the lock sweep plus git reset. It keeps your local and sandbox scripts almost identical, which is its biggest advantage.
  • Predictable failures after every VM restart: add workaround 2 — the writable-directory probe. Ownership problems do not heal with a single rm; you need a different parent directory.
  • Jobs that must not fail: switch to workaround 3 — the REST API path. You give up local git ergonomics, but you also give up every category of lock-related failure at once.

The trap I fell into early on was layering all three preemptively. The script became hard to read, the symptoms got harder to reason about, and I couldn't tell which layer was actually saving me. Diagnose first, then add only the workaround that matches the layer where the failure lives.

A 30-second sanity check before you blame git

When the failure first shows up, it's worth ruling out a few cheap explanations before you reach for any of the workarounds above. I keep this short diagnostic snippet in my notes:

# Who owns this directory?
ls -ld . .git
 
# Is anything actually holding the lock?
ls -la .git/*.lock 2>/dev/null
fuser .git/index.lock 2>/dev/null  # may not be available in sandboxes
 
# How much disk is left? ENOSPC fakes a lock symptom on some systems
df -h .

If ls -ld . shows an owner that doesn't match the user from whoami, you're looking at workaround 2. If df -h . shows under 50 MB free, every workaround above will eventually fail — clean up before you do anything else. And if fuser reports a live PID, do not delete the lock; let that command finish or kill the process first.

Building this 30-second check into your script means you stop chasing ghost problems. Most of my "weird sandbox bugs" turned out to be one of those three lines telling me exactly what was wrong.

The single most useful thing you can do right now is add workaround 1's four lines in front of your existing git add. You'll feel the difference in your weekly failure rate within days.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-10
When git add -A Sweeps Up .bak Backups — The Quiet Trap of In-Place Fix Scripts
How .bak backups left by sed -i and homegrown --fix scripts get silently swept into production by git add -A, why it is so easy to miss, and how scoped adds and .gitignore close the gap for good.
Claude Code2026-06-03
When git push Says Success but Nothing Lands — the Silent commit Failure in Claude Code
git push prints Everything up-to-date and exits zero, yet your changes never reach the remote. Here is why commit silently fails on a fresh sandbox clone, and how to verify a real push with SHA comparison.
Claude Code2026-06-01
Fixing 403 Write Access to Repository Not Granted When Pushing from Claude Code
When Claude Code automation pushes to GitHub and hits a 403 'Write access to repository not granted', the cause usually lies in how fine-grained PATs differ from classic ones. Here's how to diagnose repository access, Contents permissions, and org approval.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →