[BUG]
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 realshutdown 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
- 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.
- 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.
- **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_