AIToday
Large Language ModelsAI Coding AssistantsAI Safety & AlignmentZenn AI/MLPublished: Oct 21, 2026, 13:00 JST

Pi's bash hook really blocks, but unlink slips past the guard

Send on LINE
Pi's bash hook really blocks, but unlink slips past the guard

3 Key Points

  1. What happened

    Testing Pi 0.84.2 with claude-sonnet-4-5, a hook returning { block: true, reason } on rm produced a normal tool error, not a crash, and rewrites to event.input.command took effect — but asking it to avoid rm led to unlink foo.tmp && unlink bar.tmp, which deleted the files.

  2. Why it matters

    A hook refusal is graceful rather than a hard stop, yet regex-based guards shift the burden of real safety onto the extension author, since Pi's framework does not guarantee semantic safety.

Not sure about something? Ask the AI

Questions and answers are published on this page.

Summaries like this, in your inbox every morning.

Context & Analysis

The test began from a narrower question than safety in general: what actually reaches the model when a hook refuses a call. Pi's documentation covered the mechanics — event.input can be rewritten in place with no re-validation, and refusals return { block: true, reason?, terminate? } — but said nothing about the result the model sees. Running a minimal extension that rejects anything matching /\brm\b/ and prefixes other bash commands with an echo marker, the author watched the session log confirm that the marker really appeared in the output, and that the rejection arrived as an ordinary error result carrying the reason string. The model then re-checked the directory with ls -la, saw the tmp files still present, and asked the user whether to continue or clean up instead of retrying endlessly.

That orderly behavior changed once the instruction was strengthened. Told that the files had to be deleted even without rm, the model avoided the blocked pattern and used unlink, which passed straight through the regex, and the author confirmed only keep.txt remained. The model also listed find -delete, perl's unlink and a python os.remove one-liner as alternatives. The author's conclusion is that being able to refuse a call and being safe are different things, and that because Pi runs extensions in-process and exposes the argument contents, stronger filters are possible — but they remain the extension author's responsibility rather than a framework guarantee.

The author also notes that a prior Japanese-language write-up already showed the refusal-and-continue core using a mock provider, whereas this run went through the real Anthropic API and additionally covered command rewriting and instruction-based bypass. The terminate: true option, parallel tool calls and a measured comparison with OpenCode's out-of-process model were left for later.

FAQ
Does blocking a command crash the coding agent?
No. With Pi, the refusal came back as a tool result with isError: true and the rejection reason as its text, and the process kept running without an exception.
Can the model get around a blocked command?
Yes. When explicitly told to delete the files another way, the model ran unlink foo.tmp && unlink bar.tmp, which the regex guard did not catch, and both files were removed.
Which version and model were used in the test?
Pi version 0.84.2 running in print mode with Anthropic's claude-sonnet-4-5.
Send on LINE

AI news that matters for your work, delivered every morning.

Pick your industry and the AI tools you use, and get news related to your work every day.

Free · 30 seconds with Google · unsubscribe anytimeWhat is AIToday? →

Ask AI

Ask AI anything about this article. The AI reads this article, earlier AIToday articles, and Wikipedia, and cites its sources. Q&As are published on this page for other readers too.

Questions and answers are published on this page.

Related Articles

Next articleOpenAI's GPT-6 Astra hits 87.39% on MultiLangBench, toping GPT-5.6 Sol