◉CLAUDE LABJP
●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 38 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 38 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Cowork
◈ Cowork/2026-10-22Intermediate

Half My Scheduled-Task Logs Were Unreadable, So I Added One Mark Line Instead of Widening the Regex

Only about half of 1,076 daily log entries yielded a result. Widening the regex recovered 35 and raised false alarms, so I wrote a fixed one-line mark per run and a morning check script.

Cowork54Scheduled Tasks18Log Design3bash6Python23

✦ Premium Article

On a Thursday night I lined up seven months of daily logs from my scheduled tasks and tried to turn them into one table.

I wanted something simple: which task, on which day, ended in SUCCESS or FAILED. I had been writing a Status: line in every log, so I assumed a single grep would do it.

About half of the entries could not be read.

files=519 entries=1076
read by stage: {'strict': 526, 'silent': 515, 'body': 24, 'header': 11}

Across 519 daily log files and 1,076 entries, only 526 gave up a result through the Status: line. For 515 entries, no outcome word appeared anywhere.

What follows is how I traced that, why I stopped widening the regex, and how to add one machine-readable line at the end of each run. If you run more scheduled tasks than you did a year ago, it may save you the same evening.

The unreadable half grew month by month

I counted the unreadable entries per month first.

MonthEntriesNo readable resultShare
March1644about 2%
May13550about 37%
June182142about 78%
August9473about 78%
October (to the 22nd)11264about 57%

March reads almost perfectly. Then June falls apart and never recovers.

The format had not been broken by accident. Every time I added a new kind of scheduled task, it started writing logs in its own shape. Of the 448 entries since June with no outcome word, 166 sit in the main daily file, 232 in the review-style files (_brushup_am and similar), and the remaining 50 in integration and troubleshooting runs.

Each log was perfectly readable to me on the day I wrote it. That is how this kind of debt works: fine for human eyes, silent trouble the first time a machine tries to read it.

The measuring script records which stage did the reading

Here is the script that produced the numbers. The useful part is that it counts not just the result but which stage managed to find it.

#!/usr/bin/env python3
"""Read one result per entry from free-form daily logs (entries separated by blank lines).
Try the stages in order and record which one succeeded (strict → header → body → silent)."""
import re, sys, glob, collections, pathlib
 
WORDS = r"(SUCCESS|FAILED|PENDING_PUSH|PENDING|SKIPPED_DUPLICATE|SKIPPED|SKIP)"
HEAD  = re.compile(r"^\[(?:\d{4}-\d\d-\d\d|\d\d:\d\d|[\w-]+ \d\d:\d\d)[^\]\n]*\]", re.M)  # only a line starting with a date or time in [..] begins an entry
STRICT = re.compile(rf"^\s*[-*]?\s*Status:\s*{WORDS}", re.M)     # the agreed format
INHEAD = re.compile(rf"^\[[^\]\n]+\]\s*{WORDS}", re.M)           # the [task 15:00] SUCCESS style
LOOSE  = re.compile(rf"\b{WORDS}\b")                             # the word appears anywhere in the entry
 
def split_entries(text):
    starts = [m.start() for m in HEAD.finditer(text)]
    if not starts:
        return [text] if text.strip() else []
    starts.append(len(text))
    return [text[a:b] for a, b in zip(starts, starts[1:])]
 
def classify(block):
    for name, rx in (("strict", STRICT), ("header", INHEAD), ("body", LOOSE)):
        m = rx.search(block)
        if m:
            return name, m.group(1)
    return "silent", None
 
def main(log_dir):
    by_method, by_status, silent_files = collections.Counter(), collections.Counter(), []
    files = sorted(glob.glob(str(pathlib.Path(log_dir) / "20??-??-??*.txt")))
    for f in files:
        blocks = split_entries(pathlib.Path(f).read_text(encoding="utf-8", errors="ignore"))
        if not blocks:
            silent_files.append((f, "empty")); continue
        for b in blocks:
            method, status = classify(b)
            by_method[method] += 1
            if status: by_status[status] += 1
            else: silent_files.append((f, "no-status"))
    print(f"files={len(files)} entries={sum(by_method.values())}")
    print("read by stage:", dict(by_method))
    print("results :", dict(by_status.most_common()))
    print("unreadable:", len(silent_files))
    for f, why in silent_files[:5]:
        print("  ", pathlib.Path(f).name, why)
 
if __name__ == "__main__":
    main(sys.argv[1])

Run it by passing the log directory.

python3 ledger_scan_status_fallback.py /path/to/log_dir

The four stages are tried in order. strict is the agreed format (Status: SUCCESS), header is a result in the heading line such as [task 15:00] SUCCESS, and body is any outcome word anywhere in the entry. If none match, the entry is silent.

The silent count is the amount of log that is mute from a machine's point of view. Try it on your own logs first; it takes about three minutes.

✦

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
✦You can now measure in about three minutes how much of your own scheduled-task logs a machine can actually read
✦You will be able to prevent false failure alarms from an incidental FAILED in the text by reading only a final mark line
✦You can now notice, with one command each morning, that a task that should have run stopped without a word
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

◈ Cowork2026-10-21
Why I Stopped Borrowing a Stale Repo and Started Cloning Fresh After a Scheduled Task Couldn't Write to Shared /tmp
A scheduled task stopped because it could not create its clone directory under /tmp. Here is how I traced it, the borrowable clone that was about 9.6 hours behind, the cost of cloning fresh every run, and a script that picks a working directory.
◈ Cowork2026-10-18
I Preloaded a Scheduled Task with head -150: 15.6 KB Read, 3.3 KB for Today
A scheduled task preloaded its daily notes with head -150, and line count turned out to be no budget at all. A date-block reader with a byte ceiling, and how to avoid dropping ideas nobody has used yet.
◈ Cowork2026-10-17
When a Scheduled Task Keeps Saying SUCCEEDED but Produces Nothing, Check Outbound Traffic in the First Minute
The run history is green, but the output file is still yesterday's. A short probe and a freshness check that tell you in the first minute whether outbound calls are being refused with a 403 on CONNECT.