◉CLAUDE LABJP
●2.1.312 — Fixes Claude overwriting very large CLAUDE.md files and skills going missing after a Bash cd●API — The Messages API maximum request size grew from 32 MB to 128 MB (10/28)●SONNET 4.5 — Retires on the Claude API on 11/30, 29 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST — Opus 4.8 fast mode is deprecated and will be removed on 11/30. Migrate to Opus 5.5●NEW — I Put a Ten-Line Promise File at the To…●2.1.312 — Fixes Claude overwriting very large CLAUDE.md files and skills going missing after a Bash cd●API — The Messages API maximum request size grew from 32 MB to 128 MB (10/28)●SONNET 4.5 — Retires on the Claude API on 11/30, 29 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST — Opus 4.8 fast mode is deprecated and will be removed on 11/30. Migrate to Opus 5.5●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-08-03Advanced

Existence Checks Pass, Writes Fail — Probing Capabilities Before an Unattended Run

A directory existing and a directory being writable are two different facts. Measured results from five broken-environment cases, why static clues cannot predict the worst of them, and a capability-probe preflight for unattended Claude Code runs.

Claude Code279Automation45Scheduled RunsError Handling6Operations24

✦ Premium Article

I opened the log first thing in the morning and found the run had stopped on line one.

It had been running the same way for months. Not a character of the code had changed. What changed was the environment underneath it: the working directory had quietly become owned by a different user, and writes to it were no longer permitted.

What actually bothered me was what came next. There was a check at the top of the script. Does the directory exist? If not, create it. If so, use it. I had written that shape for years without questioning it.

That check passed cleanly that morning. The directory existed.

It just could not be written to.

Existing Is Not the Same as Being Usable

I reproduced the state locally first — a directory whose owner keeps read and execute but loses write.

mkdir locked && chmod 500 locked
 
mkdir -p locked; echo "mkdir -p exit=$?"
echo "-d test: $([ -d locked ] && echo true || echo false)"
echo "-w test: $([ -w locked ] && echo true || echo false)"
touch locked/x 2>/dev/null; echo "touch exit=$?"

The output:

mkdir -p exit=0
-d test: true
-w test: false
touch exit=1

The zero from mkdir -p is the part that surprised me. It does not mean "created and ready." It means "already there, nothing to do." That is correct by the POSIX definition. But anyone who writes mkdir -p "$DIR" && cd "$DIR" reads that zero as a guarantee about the next step.

[ -d ] has the same shape. It is a question about existence, and I had been leaning on it as a question about permission. You do not get answers to questions you never asked.

That gap is precisely where the run broke.

Measuring Existence Checks Against Capability Probes

So I split verification into two styles and compared them.

The first is the existence check I had always written: os.path.isdir, os.path.exists, the presence of a .git directory. The second is a capability probe — perform the operation you are about to perform, once, at the smallest possible scale. If you plan to write, write. If you plan to delete, delete. If you plan to use git, run git rev-parse through it.

I built five environments that break in different ways: a directory that exists but rejects writes, a path that is a file where a directory was expected, a dangling symlink, a directory containing a .git entry that is not a repository, and a genuinely read-only mount.

Here is what came back.

CaseExistence checkCapability probe
Exists but not writableOKNG
File where a directory was expectedNGNG
Dangling symlinkNGNG
Has .git but is not a repositoryOKNG
Read-only mountOKNG

Three of five — 60% — passed on existence alone. And all three share a trait: the path is entirely correct, only the operation is impossible. You can stare at the spelling of that path forever and find nothing.

I also recorded what happens if you proceed anyway.

CaseActual failure
Not writabletouch: cannot touch ...: Permission denied
File where a directory was expectedtouch: cannot touch ...: Not a directory
Has .git but is not a repositoryfatal: not a git repository (exit 128)
Read-only mounttouch: cannot touch ...: Read-only file system

The painful part of an unattended run is when these surface. All the heavy work completes, and only the final write fails. In this repository, the stage that reads and tallies 1,594 article files (about 19.29 million characters) takes 0.093 seconds. The five capability probes together take 2.57 milliseconds. If your first stage takes minutes instead of milliseconds, those minutes are thrown away in full.

As a share of that first stage, the probes cost roughly 2.8%. Across three consecutive runs, the ratio between "finish everything, then fail" and "probe first, then fail" measured 6,470x, 8,315x, and 6,892x. Probes are cheap because they perform the real operation at a trivial scale rather than reasoning about permissions. There is no inference to get wrong.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Five broken-environment cases measured side by side: three passed the existence check while the operation was impossible
✦A directory where create, append, and rename all succeed but unlink fails with EPERM — and where mount options, ST_RDONLY, the sticky bit, and ownership all report it healthy
✦How a probe that read a credential differently from production rejected a perfectly valid token, and the rule that prevents it
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

⟐ Claude Code2026-08-22
Handing a long job to another session — and the completion marker for when the notification never arrives
How to use notify_when_idle to hear when another Claude Code session finishes, and a small completion marker that keeps you from waiting forever when the notification is dropped.
⟐ Claude Code2026-10-23
When a Nightly claude -p Said MCP Needed Authentication, I Added a Preflight That Splits Sleep From Keychain
Claude Code 2.1.307 fixed MCP failures caused by macOS sleep and a locked login keychain. Here is a preflight script that helps with or without the fix, and a way to match failures against sleep history.
⟐ Claude Code2026-09-03
Aligning Log Timezones at Display Time, or Fixing Them at Write Time
AdMob, the store reports, and my own logs each ended the day at a different moment. Here is how I went back and forth between display-side and write-side timezone handling, and what I settled on.