CLAUDE LABJP
PRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yetPRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yet
Articles/Cowork
Cowork/2026-05-15Intermediate

git clone Fails in Cowork Scheduled Tasks — Diagnosing /tmp Permission Errors and Disk Space Issues

When git clone fails with Permission denied or No space left on device inside a Cowork VM, the cause is usually subdirectory ownership under /tmp. Here is how to detect it correctly, plus two real bugs hiding in the usual workaround code.

Cowork36troubleshooting89git17VM2disk-managementscheduled-tasks5

A scheduled task that should have published overnight produced nothing by morning. The log contained exactly one useful line:

fatal: could not create work tree dir '/tmp/repos/claudelab.net': Permission denied

/tmp is supposed to be world-writable. Disk space was fine — over 1,600 MB free. And yet the clone was rejected. The cause turned out to be the subdirectory: /tmp/repos/ itself was owned by nobody:nogroup, and the session user had no write access to it.

I have hit this several times, and each time I felt the same specific disappointment: the workaround I had already written was somehow not firing. Building automation as an indie developer, this kind of environment-dependent failure always arrives quietly. So what follows covers more than the root cause. It also covers the defects hiding inside the workaround itself. Every shell behavior shown here was re-run on a Cowork VM when the symptom came back.


Why /tmp/repos Ends Up Owned by nobody

Inside a Cowork VM, subdirectories under /tmp/ can survive between sessions carrying the ownership of whoever created them. If an earlier session created /tmp/repos/ as nobody:nogroup, the next session runs under a different user context and cannot write into it.

The detail that matters is how far the sticky bit reaches. /tmp/ itself is drwxrwxrwt, so anyone can create entries inside it. But /tmp/repos/, created as an ordinary directory, is rwxr-xr-x — writable by its owner only.

Here is what the VM actually looked like when the failure recurred:

$ ls -ld /tmp /tmp/repos
drwxrwxrwt 31 nobody nogroup 4096 /tmp
drwxr-xr-x  3 nobody nogroup 4096 /tmp/repos

$ id
uid=1296(trusting-keen-edison) gid=1296(trusting-keen-edison)

Anyone can write to /tmp. Only nobody can write to /tmp/repos. And you are not nobody. Laid out in three lines it is obvious, but the error message points at /tmp/repos/claudelab.net — one level deeper than the actual problem — which is why it takes a moment to see.


mkdir -p Succeeds, So You Find Out Too Late

What makes this failure mode slippery is that the step before the clone quietly succeeds.

$ mkdir -p /tmp/repos
$ echo $?
0

mkdir -p returns 0 when the target already exists. It never checks whether you can write to it. Which means this very common line looks like a safety net but never actually fires:

# ❌ Does nothing
mkdir -p /tmp/repos 2>/dev/null || WORK="$HOME/repos/claudelab.net"

When /tmp/repos exists and is owned by nobody, mkdir -p exits 0, the right side of || is skipped entirely, and execution proceeds to git clone — which is where Permission denied finally surfaces. That is precisely why my "existing workaround" appeared to be doing nothing. It was doing nothing.

The test has to be write access itself, not creation success:

# ✅ Actually detects the condition
[ -d /tmp/repos ] && [ ! -w /tmp/repos ] && echo "not writable"

Evaluated on the VM above, this pair of tests reported "not writable" and the fallback fired as intended. -w works on directories, so no extra tooling is needed.

In my experience three situations produce this state. Right after a Cowork update or session restart swaps the internal user context. When several scheduled tasks share the same /tmp/repos/ and whichever runs first creates it as nobody. And after a task crashes partway through a git operation, leaving a half-written directory behind. The common thread is that the previous run leaks into the current one. Scheduled tasks look like they start from a clean slate, but /tmp/ is shared across sessions.


The Fix: Falling Back to $HOME/repos

The reliable answer is to switch to $HOME/repos/ automatically whenever /tmp/repos/ is unusable.

# Decide the work directory by write access, not by mkdir success
if [ -d /tmp/repos ] && [ ! -w /tmp/repos ]; then
  echo "⚠️ /tmp/repos not writable (likely nobody-owned) — using $HOME/repos"
  WORK="$HOME/repos/claudelab.net"
elif mkdir -p /tmp/repos 2>/dev/null && [ -w /tmp/repos ]; then
  WORK="/tmp/repos/claudelab.net"
else
  WORK="$HOME/repos/claudelab.net"
fi
 
mkdir -p "$(dirname "$WORK")" || { echo "⛔ cannot create work directory"; exit 1; }
echo "📁 Working directory: $WORK"

Note that even the elif branch does not trust mkdir -p alone — it follows up with [ -w /tmp/repos ]. Hold on to the single idea that creating and writing are different questions, and the whole branch reads naturally.

In a Cowork VM, $HOME resolves to /sessions/<session-name>/, which the current session owns, so writes there always succeed.


The Cleanup Code That Deletes Your Working Directory

Many of us add a "remove old repositories when disk runs low" step. I did too. The usual shape looks like this:

# ❌ Dangerous: takes the working directory with it
find "$(dirname "$WORK")" -maxdepth 1 -type d \
  -not -name "$(basename $WORK)" \
  -exec rm -rf {} + 2>/dev/null

The intent is "delete everything inside /tmp/repos/ except the repo we are about to use." But find includes the starting point in its results. The basename of /tmp/repos is repos, not claudelab.net, so it slips past the -not -name filter and the parent directory itself becomes an argument to rm -rf.

I built a scratch tree and ran it:

# Before
/tmp/t1/repos/claudelab.net/KEEPME
/tmp/t1/repos/gemilab.net
/tmp/t1/repos/rorklab.net

# Run the command above

# After
ls: cannot access '/tmp/t1/repos': No such file or directory
KEEPME — gone

The directory it was supposed to protect went with everything else. Worse, this branch only executes when free space drops below the threshold, so ordinary test runs never reach it. It waits for the one day the disk is tight, then quietly destroys the workspace.

The fix is a single flag. -mindepth 1 excludes the starting point:

# ✅ Safe: skip the starting point
find "$(dirname "$WORK")" -mindepth 1 -maxdepth 1 -type d \
  -not -name "$(basename "$WORK")" \
  -exec rm -rf {} + 2>/dev/null

Re-running the same scenario left claudelab.net and KEEPME intact while removing the other repositories. Quote "$WORK" as well — an unquoted path containing a space will split the argument to basename.


A Combined Diagnostic Including Disk Exhaustion

Permission denied and No space left on device both stop a clone, but they call for different responses. Diagnosing both at the top of a scheduled task makes the morning log much faster to read.

There is one more trap in the disk check. Passing df a directory that does not exist yields empty output, and ${FREE_MB:-0} then evaluates to 0 — a false "disk full." The measured behavior:

$ FREE_MB=$(df /tmp/does-not-exist --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
$ echo "[${FREE_MB}] -> ${FREE_MB:-0}"
[] -> 0

On a first run the work directory normally does not exist yet, so either create it before calling df or handle the empty string explicitly. The block below does both:

WS="$(ls -d /sessions/*/mnt/Dolice\ Labs 2>/dev/null | head -1)"
GITHUB_TOKEN=$(grep -A1 "Claude Lab" "${WS}/_documents/_github_tokens/github_tokens.txt" \
  | tail -1 | tr -d '[:space:]')
REPO_URL="https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git"
 
# --- 1. Choose the work directory by write access ---
if [ -d /tmp/repos ] && [ ! -w /tmp/repos ]; then
  WORK="$HOME/repos/claudelab.net"
elif mkdir -p /tmp/repos 2>/dev/null && [ -w /tmp/repos ]; then
  WORK="/tmp/repos/claudelab.net"
else
  WORK="$HOME/repos/claudelab.net"
fi
BASE="$(dirname "$WORK")"
mkdir -p "$BASE" || { echo "⛔ cannot create $BASE"; exit 1; }
 
# --- 2. Disk check, treating empty output as unknown ---
FREE_MB=$(df "$BASE" --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
if [ -z "$FREE_MB" ]; then
  echo "⚠️ could not read free space for $BASE — skipping cleanup"
else
  echo "📊 Available: ${FREE_MB}MB (target: $WORK)"
  if [ "$FREE_MB" -lt 300 ]; then
    echo "⚠️ Low disk — removing other repositories"
    find "$BASE" -mindepth 1 -maxdepth 1 -type d \
      -not -name "$(basename "$WORK")" \
      -exec rm -rf {} + 2>/dev/null
    FREE_MB=$(df "$BASE" --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
    echo "📊 After cleanup: ${FREE_MB:-unknown}MB"
  fi
  if [ "${FREE_MB:-0}" -lt 200 ]; then
    echo "⛔ Still not enough space — aborting"
    exit 1
  fi
fi
 
# --- 3. Clone or pull ---
if [ -d "$WORK/.git" ]; then
  cd "$WORK" || exit 1
  git remote set-url origin "$REPO_URL"
  git pull --rebase origin main || {
    cd "$BASE" || exit 1
    rm -rf "$WORK"
    git clone --depth 1 "$REPO_URL" "$WORK" || exit 1
    cd "$WORK" || exit 1
  }
else
  git clone --depth 1 "$REPO_URL" "$WORK" || exit 1
  cd "$WORK" || exit 1
fi
 
git config user.email "masakihirokawa@gmail.com"
git config user.name "Masaki Hirokawa"
echo "✅ Repository ready"

The cd "$BASE" before rm -rf "$WORK" in the recovery path matters: calling git clone while your shell still sits inside a deleted directory fails on some systems. It is a small thing, but recovery paths are exactly where small things bite.


Choosing Between /tmp and $HOME

Rather than argue about it, I measured. One repository, Claude Lab:

OperationElapsedNotes
git clone --depth 1 (fresh)~3.8 s45 MB including the working tree
git pull --rebase (no changes)~0.8 sagainst the existing shallow clone

Three seconds per run. For a one-off script that is noise. Across four sites running several times a day it accumulates — but the more important difference is that a clone depends on the network every single time. Incremental updates are far less likely to fail during a flaky window.

Whether git pull --rebase origin main is safe against a --depth 1 clone is a fair question, and one I wanted to confirm rather than assume. It worked without complaint: git rev-parse --is-shallow-repository still reported true afterward, and the local commit count stayed at 1. If your task never needs history, staying shallow and pulling diffs is fine.

As a rule of thumb, I use /tmp for one-off scripts and anything that should leave no trace, and $HOME/repos/ with a persistent repository for scheduled tasks that run several times a day. Even with /tmp, keep the writability check.


Quick Error Reference

The message almost always identifies the cause:

ErrorCauseFirst step
Permission deniedParent directory ownershipCheck owner with ls -ld, switch to $HOME/repos/
No space left on deviceDisk exhaustionRun df -h, remove other repos and .next/
Could not resolve hostNo network accessCheck the VM's internet access setting
Repository not foundExpired token or renamed repoRegenerate the PAT and update SKILL.md too
index.lock: File existsPrevious git process crashedDelete .git/index.lock or re-clone

One note on Repository not found: an invalid token can return the same wording. Check the token's expiry before you start hunting for a typo in the repository name — it gets you there faster.


Updating Your SKILL.md

If your scheduled tasks are driven by a SKILL.md, put this at the very top of the Step 0 repository setup so a recurrence routes around itself:

# Top of Step 0 — decide by write access
_BASE="/tmp/repos"
if [ -d "$_BASE" ] && [ ! -w "$_BASE" ]; then
  _BASE="$HOME/repos"
elif ! mkdir -p "$_BASE" 2>/dev/null || [ ! -w "$_BASE" ]; then
  _BASE="$HOME/repos"
fi
mkdir -p "$_BASE" || exit 1
WORK="${_BASE}/claudelab.net"

After editing SKILL.md, apply the same change to the scheduled task prompt. They are loaded as separate documents, so fixing only one means the next run reverts to the old behavior. It is invisible during manual testing — I once spent a second morning looking at a bug I had already fixed.


What to Do Right Now

Run ls -ld /tmp/repos. If the owner column reads nobody nogroup, replace the mkdir -p success check in Step 0 with a -w test. That single change does the most work here.

Then open your cleanup code and look for -mindepth 1 in the find call. If it is missing, add it. A step that only fires on a tight-disk day and removes your workspace is the kind of bug that stays hidden for months.

Both edits take a few minutes. Automation proves itself not when nothing is happening, but when something is.

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 $15 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

Cowork2026-06-30
Trusting a Three-Day-Old Mirror: Stopping Unattended Tasks from Acting on a Stale Working Copy
A persistent clone reused for speed quietly drifts from the remote. Read 'has this article been fixed yet?' from an out-of-sync tree and your unattended task duplicates work or 'succeeds' against an old world. Here is a HEAD-match plus writability preflight, with a self-healing re-clone, in bash.
Cowork2026-05-23
When pip install Stops with externally-managed-environment in Cowork's Bash Sandbox — Three Patterns for PEP 668
Why pip install fails with externally-managed-environment in Cowork's Bash sandbox, and three practical fixes — --break-system-packages, venv, and pipx — written from real scheduled-task experience.
Cowork2026-05-06
Claude in Chrome Not Working? A Practical Troubleshooting Guide
Buttons that do nothing, pages that never load, tasks that report success but change nothing. Symptom-by-symptom fixes, plus the 42-run log where adding waits and verification steps moved completion from 62% to 95%.
📚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 →