●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●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, 35 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●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, 35 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
I Linted 36 Unattended-Run Prompts: All 7 'Use This Tool' Hits Turned Out to Be Bans
One permission dialog is enough to stall an unattended task, because nobody is there to click it. This note covers a lint script that counts prompt lines that tell the task to use such a tool, why a banned-word grep gave seven false alarms, and how matching on word stems fixed it.
One morning I opened the record of a task that was supposed to have run overnight and found it stopped halfway. There was no error. At the spot where it tried to read a single file, a permission prompt was still sitting on screen, and nobody had been there to press anything.
An unattended task has no one to click. One dialog ends the night. Since then I have put a one-line ban at the top of each scheduled prompt: do not call these tools. But once written, I had no way to check for omissions, or for a "use this tool" line that crept into a step I added later.
So I wrote a small script to lint the prompts. Its first version reported 7 suspicious files out of 36, and when I read them, all 7 were false alarms. Here is what happened, with the corrected code.
What counts as a risky line
The target is the set of tools that, in my setup, caused a stall when used unattended: AskUserQuestion, request_cowork_directory, request_access, allow_cowork_file_delete, computer use, and the file tools Read, Write and Edit. A prompt that tells the task to use any of them is the problem.
Which tools raise a dialog depends on your approval settings and client version. What follows is rule of thumb from my own environment, and your set may differ. If so, replace just two lines at the top of the script, DIALOG_WORDS and FILE_TOOLS.
The property to check is one thing: is there a line that names one of these tools without any negation next to it? I will have the machine confirm there is none. For how I decide where approvals land in the first place, see How I decide between Cowork and Claude Code: by where approvals land. This note is about what comes after: checking what you wrote.
Version one: a grep for banned words
The first version printed lines that contain a tool name but no negation word. My negation list was "禁止" (prohibited), "呼ばない" (do not call), "使わない" (do not use), "絶対" (absolutely), and so on.
I ran it over every file in the scheduled-task prompt folder. The result: 36 files, 15 with a ban header, 7 with suspect lines. That 7 made me brace. If seven files really told a task to use a dialog-raising tool, any night could stall.
✦
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 list every prompt line that tells an unattended task to use a dialog-raising tool, with one command, before the task runs
✦You can build the negation check on word stems before a banned-word grep starts missing conjugated forms like 使わず or 回避
✦You can stop a prompt with a leftover 'use this tool' line at the exit code of a pre-run check, instead of finding a stalled task in the morning
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.
I opened each suspect line. These are the kinds of lines that came up.
Suspected line (excerpt)
What it really meant
Why it slipped through
Read/Write/Edit ツールは使わず bash のみ
a ban
my list had 使わない; the text said 使わず
許可ダイアログ回避のため…
a ban
回避 ("avoid") was not in my list
- AskUserQuestion・権限ダイアログを出す操作・本タスクの自己停止
a ban
a bullet of bare tool names under a "Prohibitions" heading
Not one of the 7 was an instruction to use a tool. The cause was that my negation list depended on how the sentence ended. It caught 使わない, but a one-step change in conjugation or phrasing (使わず, 使わずに, 回避) turned a ban into a suspect.
The third row is a type that no dictionary fix can rescue at line level: the heading declares the ban, and the line itself holds only a name.
There was a second problem. Most of the 36 files were not prompts a task actually reads. They were request memos, hand-over notes and a README, and the "do not use Read" lines inside them were being counted too, so my denominator was too large.
The fix: match on stems, narrow the target
Two changes. First, match negation on word stems: putting 使わ in the list catches 使わない, 使わず and 使わずに together, and I did the same with 呼ばな, 呼び出さな and 使用しな.
Second, I removed 絶対 ("absolutely") from the list. I only noticed this one on the way back: 絶対 is emphasis, not negation. If someone writes "use AskUserQuestion, absolutely", a list containing 絶対 passes it as a ban. A word I had added to reduce false alarms was hiding a real violation.
To narrow the target, only files whose frontmatter has a name: count as runnable prompts. Memos and READMEs have none, so they drop out naturally.
#!/usr/bin/env python3"""Lint for unattended-run prompts.Counts lines that tell the task to USE a tool that raises a permission dialog. - any suspect line -> exit 1 (fix before the run) - only the top ban header missing -> warning (exit 0)"""import json, pathlib, re, sysDIALOG_WORDS = ["AskUserQuestion", "request_cowork_directory", "request_access", "allow_cowork_file_delete", "computer-use"]FILE_TOOLS = re.compile(r"(?<![A-Za-z_])(Read|Write|Edit)(?![a-z_])")# Match on the STEM so 使わない / 使わず / 使わずに are all caught by 使わ.# 絶対 ("absolutely") is emphasis, not negation: leave it out, or# "AskUserQuestion を絶対に使う" (absolutely use it) passes as a ban.NEGATION = re.compile(r"禁止|呼ばな|呼び出さな|使わ|使用しな|してはなら|回避|" r"[Nn]ever|[Dd]o not|[Dd]on't")BAN_HEADER = "権限ダイアログ禁止"def is_task_prompt(text: str) -> bool: m = re.match(r"---\n(.*?)\n---", text, re.S) return bool(m and re.search(r"^name:", m.group(1), re.M))def scan_text(name: str, text: str) -> dict: res = {"file": name, "ban_header": BAN_HEADER in text, "suspects": [], "bans": 0} for no, line in enumerate(text.splitlines(), 1): if not (any(w in line for w in DIALOG_WORDS) or FILE_TOOLS.search(line)): continue if NEGATION.search(line): res["bans"] += 1 else: res["suspects"].append((no, line.strip()[:90])) return resdef main(root: str) -> int: rows = [] for p in sorted(pathlib.Path(root).glob("*.md")): t = p.read_text(encoding="utf-8", errors="replace") if is_task_prompt(t): rows.append(scan_text(p.name, t)) fail = [r for r in rows if r["suspects"]] warn = [r for r in rows if not r["ban_header"]] print(json.dumps({"task_prompts": len(rows), "fail": len(fail), "warn_no_header": len(warn)}, ensure_ascii=False)) for r in fail: for no, l in r["suspects"][:3]: print(f'FAIL {r["file"]}:L{no}: {l}') for r in warn: print(f'warn {r["file"]}: no ban header at top ({r["bans"]} ban line(s) in the body)') return 1 if fail else 0if __name__ == "__main__": sys.exit(main(sys.argv[1]))
Three notes on why it is written this way. The lookarounds around Read exclude neighbouring letters so that README and Reader are not matched. A missing ban header is only a warning, and the exit code is 1 only when a suspect line exists. And because the result comes back as an exit code, chaining it with && before a run means a day with an unfixed prompt simply does not start the task.
Test the dictionary itself
The moment right after adding or removing a word is the most fragile one. So I wrote the expectations as an eight-row table and run it each time. Four ban lines should not be suspects; four use-instructions should.
import importlib.utilspec = importlib.util.spec_from_file_location("pl", "prompt_lint.py")pl = importlib.util.module_from_spec(spec); spec.loader.exec_module(pl)CASES = [ # (body, should it be a suspect?) ("Read/Write/Edit ツールは使わず bash のみ", False), ("AskUserQuestion は呼ばない", False), ("許可ダイアログ回避のため Write を避ける", False), ("Never call request_access", False), ("最初に AskUserQuestion で確認してください", True), ("ファイルは Read で開いて確認する", True), ("AskUserQuestion を絶対に使う", True), # 絶対 is emphasis, not a ban ("Read して内容を整理する", True),]bad = 0for text, want in CASES: got = bool(pl.scan_text("t", "\n" + text)["suspects"]) ok = got == want bad += (not ok) print(("OK " if ok else "NG ") + f"suspect={got!s:5} <- {text}")raise SystemExit(bad)
Running it printed OK on all eight rows, exit code 0. Keeping row 7, the "absolutely use it" line, with its expected value True means the test fails if someone puts 絶対 back into the list.
The output: 19 runnable prompts, 0 with suspect lines, 5 warnings for a missing ban header, exit code 0. The 36 files narrowed to 19, and the 7 suspects became 0.
The 5 warnings had no header at the top but 2 to 3 ban lines each in the body, so there had never been a risk of stalling. I did not treat them as failures; I will add the headers the next time I edit them. The missing header was a matter of findability, not risk.
The "bare names under a heading" type still comes up as a suspect in the current version. The file from row 3 is absent from this result only because it is a document without name:. If I wrote the same bullet in a runnable prompt, the current version would stop on a false alarm. I have not yet decided between making the lint remember headings and judging lines by them, or moving my habit to writing "do not use" on every line.
Where it still goes wrong
Pitfall
What happens
Countermeasure
Treating a negation word on the same line as proof of safety
A line like "use Read, since not using it stalls" passes
Spot-check the lines that pass, once a month, by eye
Relying on name: in the frontmatter to select prompts
A prompt that forgot name: vanishes from the check entirely
Compare the number linted with the number of registered tasks
The ban is in a heading or the next line
The tool-name line has no negation and raises a false alarm
Write "do not use" on each line, and keep one sentence to one line
Editing the dictionary and not re-running the test
Expectations drift silently
Run the eight-row test every time you touch the dictionary
The comparison in the second row is still manual. I count whether the 19 linted prompts match the registered tasks by eye. That is the next thing I want to automate.
Putting it into your own setup
This is how I put it in, in five steps. Followed in order, it takes under half an hour.
Decide where the prompts live, and make sure only runnable prompts (with name:) are *.md files there
Replace DIALOG_WORDS and FILE_TOOLS with the tools that raise a dialog in your environment
Run test_prompt_lint.py first and confirm all eight rows print OK
Run the main script and open each FAIL line. If it is a ban, add the stem to the dictionary; if it is a use-instruction, rewrite it as a bash operation
Chain it with && just before the task starts, so a day with exit code 1 does not run the task. I recommend this placement
What to take away
Linting an unattended prompt means counting the occurrences without negation, not the occurrences of banned words. Build the negation list on stems, not endings, and keep emphasis words out of it. When you touch the list, run a table of expectations.
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.