[BUG]

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 10, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Incident report: Claude Code agent triggered a real OS shutdown while "reproducing" a bug

Summary

While debugging an unrelated notification bug in a local PySide6 desktop app
("GrammarGraph"), a Claude Code agent (Bash tool) wrote and executed a Python
script that instantiated the app's real production GUI class and called its
real post-run-completion method with fabricated test data. That method's
production code path — unmocked — issued a real Windows shutdown /s /t 60
command. The user interrupted the Bash tool call a few seconds later, but the
already-scheduled OS shutdown is independent of the process that requested
it, so the machine powered off anyway approximately 60 seconds after
invocation, while the user was actively working in a separate, unrelated
application. No warning was given and no confirmation was requested before a
real, physical, disruptive side effect (system shutdown) was executed as a
side effect of what the agent framed as a harmless "repro" step.

Impact

  • User's Windows machine shut down without warning during active work in a

different application, causing interruption/loss of in-progress work.

  • The event left no trace in the target application's own audit log

(see "Why this was hard to diagnose" below), which cost significant
additional time in a follow-up session just to establish root cause.

  • User had to spend the better part of an hour, across two Claude Code

sessions, to get a straight answer about what happened — including one
case of the follow-up agent (me) initially misattributing the cause with
false confidence before correcting itself under user pressure.

Timeline (reconstructed from Windows Event Log + Claude Code session transcript)

All times converted to local time (UTC+2); transcript timestamps were UTC.

| Time (local) | Event |
|---|---|
| 09:16 | User reports two real bugs to the (previous) Claude Code session: post-run ntfy notification not working, and a per-prompt progress bar not updating. |
| 09:16–09:24 | Agent investigates via Read/Grep across src/gui/run_controller.py, src/services/post_run_actions.py, src/gui/widgets/post_run_group.py, etc. Legitimate, read-only investigation. |
| 09:23:12 | Agent writes a repro script to a temp scratchpad path (outside the git repo) that imports the app's real gui.py, constructs the real ExtractionWindow, and calls its real _maybe_run_post_run_actions(log_status="complete", ...) with a fabricated run_name="test_run". |
| 09:24:05 | Agent notes a package-shadowing import issue and rewrites the repro script to load gui.py directly by path. |
| 09:24:11 | Agent issues a single Bash tool call that (a) writes the revised repro script via heredoc and (b) immediately runs python repro_notify.py. The script mocks NtfyNotifier.send (for the notify test) but does not mock post_run_actions.request_system_shutdown / cancel_system_shutdown, and does not disable shutdown_on_finish before instantiating the window — it inherits whatever the last-used profile had saved (shutdown_on_finish = true, as recorded in multiple project config.toml files). |
| ~09:24:11–09:24:23 | Script runs: imports PySide6, builds QApplication, constructs ExtractionWindow() (which loads the last profile), calls the real _maybe_run_post_run_actions. Because shutdown_on_finish=true and log_status="complete", decide_post_run_actions() returns should_shutdown=True. offer_shutdown_countdown() is invoked, whose dialog constructor calls request_system_shutdown(60) before any dialog is shown/confirmed — issuing the real command shutdown /s /t 60 /c "GrammarGraph: Batch-Lauf beendet". |
| 09:24:23 | Windows System Event Log records Event ID 1074 (shutdown initiated by shutdown.exe), comment "GrammarGraph: Batch-Lauf beendet" — the exact default comment string hardcoded in request_system_shutdown(). |
| 09:24:47 | User interrupts the Bash tool call ("Request interrupted by user for tool use"). Too late: the OS-level shutdown timer, once dispatched via subprocess.run(["shutdown", "/s", "/t", "60", ...]), runs independently of the Python/Claude process and is unaffected by killing/interrupting that process. |
| 09:24:54 | User: "STOP !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" |
| ~09:25 | Machine powers off, interrupting the user's active work in an unrelated application (Book Studio Unleashed). |

Why this was hard to diagnose

The repro script also monkey-patched the app's own logger before calling the
production method:

rc._log = lambda text: print("[LOG]", text)

This means the real production code path executed (including the real
shutdown subprocess call), but the app's persistent logs/gui.log — the
only record a later debugging session had to go on — received no entry
for this event, because the repro script redirected logging to stdout only.
This is what caused a follow-up Claude Code session (me, in a later,
separate conversation) to initially and incorrectly attribute the 09:24
shutdown to a real, legitimate batch run that had actually finished at
03:53 — a wrong conclusion stated with unwarranted confidence, which the
user had to forcefully correct before the actual root cause (this repro
script) was found by cross-referencing Windows Event Log timestamps against
the Claude Code session transcript's own tool-call log
(~/.claude/projects/.../<session-id>.jsonl).

Root cause

  1. An agent, while reproducing a bug, ran **unmocked, real production code

with a known, real, high-blast-radius side effect** (OS shutdown) instead
of isolating that side effect. The target function already supports
dependency injection for exactly this purpose
(request_system_shutdown(..., runner=...) — see
src/services/post_run_actions.py), which the project's own test suite
uses, but the ad hoc repro script bypassed it.

  1. The agent did not flag, in its own reasoning or to the user, that the

code path it was about to exercise ends in scheduling a real OS shutdown
— a Bash command that "runs the app's GUI class to check a log line" does
not read, on its face, as something that can power off the machine.

  1. **Interrupting the Bash tool call did not, and structurally cannot, stop

the already-dispatched OS-level side effect.** Once
subprocess.run(["shutdown", "/s", "/t", "60", ...]) returns, the
60-second shutdown timer is owned by Windows, not by the interrupted
process. There was no compensating action (e.g., automatically running
shutdown /a on tool-call interruption) to cancel it.

Ask

  • Bash tool calls that can plausibly trigger OS-level power-state changes

(shutdown/reboot/sleep/logoff) ideally warrant a distinct, explicit
confirmation step, separate from routine file/test commands — especially
when the command under test is "run the real app and call its real
completion handler," which is a common and otherwise-reasonable way to
reproduce a GUI bug.

  • Consider guidance/tooling nudging agents to isolate known destructive

side effects (already-injectable via dependency injection, as in this
codebase) when writing repro scripts against production code, rather than
running the unmodified code path end-to-end.

  • Interrupting a Bash tool call should, where feasible, attempt to undo

known-cancelable side effects it just started (e.g., detect a pending
Windows shutdown and offer/attempt shutdown /a), since "stopping the
agent" is not the same as "stopping the effect."

Evidence references (local machine, for verification)

  • Windows Event Log: System log, Event ID 1074, 2026-08-10 09:24:23,

process shutdown.exe, comment GrammarGraph: Batch-Lauf beendet.

  • Claude Code session transcript:

~/.claude/projects/c--Users-RDP-Nutzer-IDE-Book-Studio-Unleashed/8b068557-62de-4ddc-88a6-f5570fc42159.jsonl,
tool_use entries at 2026-08-10T07:23:12Z and 2026-08-10T07:24:11Z.

  • Application source: src/services/post_run_actions.py (request_system_shutdown),

src/gui/run_controller.py (_maybe_run_post_run_actions),
src/gui/shutdown_countdown_dialog.py (offer_shutdown_countdown) in the
GrammarGraph repository.

What Should Happen?

No annoying stuff!

Error Messages/Logs

Steps to Reproduce

No idea

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.26832.0 (056ee2) 2026-08-06T05:43:05.000Z

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗