FIRE-1947 | Cowork block modal (stacked on #40) - #41
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Merged
c31ee5a deleted security-alert.{sh,ps1} and the modal-firing branches because "Claude Desktop now displays hook blocks natively, so the plugin is pure relay". That was correct for the surfaces that existed then, and is still correct for the CLI and the Desktop app. Claude Cowork, which was not a surface when that commit landed, breaks the claim: its client discards hook-authored text on EVERY documented channel. Measured 2026-08-18, one session per surface, with a probe plugin recording its own uname/whoami/HOME/process-ancestry/env from inside the hook process: channel cowork cloud cowork local {"decision":"block","reason":R} spinner hangs, no text stops, NO text {"continue":false,"stopReason":R} spinner hangs, no text stops, NO text systemMessage alongside a block - stops, NO text exit code 2 + message on stderr - stops, NO text hookSpecificOutput.additionalContext renders (model relays) renders native osascript modal impossible (no GUI) WORKS Four independent documented channels reach the same dead end, so no JSON shape fixes this. In a local session the OS modal is the only channel that reaches the user, and additionalContext is not a substitute (it reaches the model, which may or may not repeat it). So the modal comes back, gated to that one surface: * _rogue_want_alert (hook.sh) / Test-WantAlert (hook.ps1), four tests, cheap first: surface == claude_cowork, then not CLAUDE_CODE_REMOTE (cloud runs the hook in a headless Linux container - root, HOME=/root, no osascript, no DISPLAY, no DBUS), then the ROGUE_ALERT / ROGUE_ALERT_EVENTS escape hatches, then `command -v osascript` as the final authority. * The surface comes from install-id.sh's ROGUE_INSTALL_AGENT rather than a second copy of the CLAUDE_CODE_IS_COWORK-first cascade. Local Cowork's entrypoint is `local-agent`, NOT a *cowork* value, so entrypoint matching alone files it as the CLI - install-id.sh already knows that, and a duplicate would drift from the roster's. * Cloud is excluded EXPLICITLY, not left to fail silently: a declined alert logs alert_skipped=1 with all four gate inputs, so hook.log stays honest about what the user was and was not told. * The relay is untouched. The modal is a side-channel; the server body still goes back verbatim and every path still exits 0. security-alert.{sh,ps1} are restored from c31ee5a^ with one deliberate change: the osascript is no longer wrapped in `tell application "System Events"`. A cross-app tell is an Automation request, so macOS attributes it to the host app and prompts '"Claude" wants access to control "System Events"' on the first block - a consent dialog in front of a security alert, which a user can deny, permanently killing it. plugins/copilot learned this under PyCharm; a bare `display alert` needs no permission and a top-level `activate` targets osascript itself. Everything else is byte-for-byte, including the bash-only \n conversion (hence `bash`, not `sh`, at the call site). Two things that must not be simplified are re-landed with their reasoning: the sh side's `( ... ) >/dev/null 2>&1 </dev/null &`, which detaches the SUBSHELL's fds and not just the alert's (dbfd3ee: without it the subshell holds Claude's read pipe open until the dialog is dismissed, Claude times the hook out and FAILS OPEN, letting the blocked prompt through), and the ps1 side's relay-then-launch ordering with -EncodedCommand. Tests: * tests/test_hook_sh.sh - 10 new end-to-end cases (cowork local fires, cloud skips, CLI/Desktop skip, no-osascript skips, allow fires nothing, a failing osascript is still fail-open, ROGUE_ALERT_EVENTS narrows, ROGUE_ALERT=0 kills). Each also asserts the body is relayed verbatim. A HANGING osascript stub regression-tests the fd detachment - verified to fail (30s wait) when the redirection is removed. Cases run against a sandboxed PATH holding no osascript, so a dev Mac's real /usr/bin/osascript can neither defeat the capability case nor pop a dialog on the tester's screen. * tests/test_hook_ps1.ps1 - 13 new cases: the gate matrix, plus static assertions that the response is relayed BEFORE the launch and that the launch stays detached/hidden/-EncodedCommand. * tests/test_hooks_json.sh passes unchanged - no hooks.json change. No version bump here: this PR is stacked on hotfix/cowork-actor-fix, which already carries 1.0.23 -> 1.0.24, and the two merge together — a second bump would ship two releases for one merge. Delete this whole path once Cowork renders the reason itself, exactly as it was deleted once Claude Desktop did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yuval-qf
force-pushed
the
hotfix/cowork-osascript
branch
from
August 18, 2026 12:58
10f41d4 to
e65e43e
Compare
…stale docs Both from PR review. P2 — alert_rc was useless for exactly the failures it exists to surface. security-alert.sh was restored verbatim from c31ee5a^, which ends both osascript branches with `|| true` and the script with `exit 0`. So the helper ALWAYS exited 0 whenever osascript was on PATH, and hook.sh's `log "alert_rc=$?"` recorded 0 for a TCC denial, an AppleScript syntax error, a missing GUI session — every case the line was added to make visible. stdout and stderr were discarded too, so nothing else carried the signal either. Reproduced with a stub osascript exiting 1: the helper still returned 0. * security-alert.sh now ends each branch `exit $?` and lets osascript's / notify-send's stderr through (only stdout — the dialog result record — is discarded). Safe because the caller backgrounds the helper in a subshell that only LOGS the status; a non-zero exit there cannot fail the hook decision, which is the same property that already lets the modal hang without harm. * The no-channel-at-all fallback now exits 127 instead of 0. An alert nobody saw must not log as a success. (Unreachable from hook.sh, whose gate already requires osascript.) * hook.sh captures the helper's stderr (`2>&1 >/dev/null`, in that order) and logs `alert_err="…"` alongside a non-zero alert_rc. A bare rc is ambiguous: osascript returns 1 for a TCC denial, a syntax error, a missing GUI session AND for "User canceled" (-128), so the reason text is what makes the status actionable. Folded to one line BEFORE sanitizing (sanitize deletes control characters, so newlines would run words together) and capped at 200 bytes. The review is also right that the old test was too weak: `assert_log 'alert_rc='` passes against the broken helper. tests/test_hook_sh.sh now asserts `alert_rc=1` exactly plus the captured stderr, and a new case asserts `alert_rc=0` with NO alert_err on a clean run. The failing stub emits two lines on stderr, so the fold-and-sanitize path is covered. Verified the new assertions FAIL against the pre-fix helper. P3 — docs/deployment.md was stale in two places, both about this modal. * The troubleshooting row told users to fix a missing modal by granting System Events Automation permission. This PR removes the `tell application "System Events"` path precisely so no Automation grant is needed, making that advice misleading for the exact failure the hotfix addresses. Replaced with the real gates (Cowork local only; CLAUDE_CODE_REMOTE unset; ROGUE_ALERT / ROGUE_ALERT_EVENTS; osascript present) and how to read alert_skipped / alert_rc / alert_err out of ~/.rogue/hook.log, stating outright that an Automation grant is not the fix. * The Windows paragraph claimed the block modal uses System.Windows.Forms.MessageBox. security-alert.ps1 deliberately uses WScript.Shell.Popup — MessageBox::Show silently no-ops from the detached hidden process the hook launches it in, which is why it was chosen. Corrected, and noted the modal is Cowork-local-only on every platform. No version change: still deferring to hotfix/cowork-actor-fix's bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drorIvry
approved these changes
Aug 18, 2026
yuval-qf
added a commit
that referenced
this pull request
Aug 18, 2026
main landed the log-shipping feature (~10k lines) on top of a released v1.0.26,
which conflicted with this branch in 9 files. Most were disjoint additions; two
were genuine semantic collisions about the SAME question — how the surface is
resolved — where each side was right about a different half:
* main introduced scripts/surface.{sh,ps1}: ONE table, because two consumers
(hook's `surface=` log slug and the heartbeat's roster `agent`) must never
name different surfaces for one session.
* this branch changed the roster `agent` from a display label to a stable
snake_case id (it doubles as the backend's PLUGIN_REPOS key, and a label
matched none, so every Claude row read as up to date), and made Cowork
detection check CLAUDE_CODE_IS_COWORK BEFORE the entrypoint — Cowork spawns
Claude Code with CLAUDE_CODE_ENTRYPOINT=local-agent, not a *cowork* value, so
entrypoint matching alone filed every LOCAL Cowork install under the CLI.
Taking either side alone loses the other's fix, and main's table has BOTH bugs
this branch fixed. Resolved by keeping the single table and giving it a third
projection:
* surface.{sh,ps1}: added rogue_surface_agent_id / Get-RogueSurfaceAgentId
(claude_code | claude_code_desktop | claude_cowork), and moved the
CLAUDE_CODE_IS_COWORK check to the FRONT of rogue_surface_slug so all three
projections agree about Cowork. The slug/label behaviour for every entrypoint
is unchanged; what changes is that IS_COWORK now wins, which is the fix.
* install-id.sh, heartbeat.ps1, hook.ps1 and skills/status/SKILL.md now READ
that projection instead of inlining a cascade — so there is one table with
three consumers rather than four copies. Fallback literals are `claude_code`,
not `Claude Code - CLI`.
Other resolutions:
* version → 1.0.27. main already RELEASED v1.0.26, so this branch's 1.0.24
would have been a downgrade: auto-update compares the manifest against the
latest release tag, so a merged 1.0.24 reads as older than what is published.
One bump for the whole stack; the stacked child (#41) carries none.
* validate.yml: both sides added disjoint steps — main runs none of this
branch's four new sh suites — so both are kept, this branch's "Shell unit
tests" after "Shell scripts parse".
* hook.ps1: both sides ended mid-function at the marker (main inside Log, this
branch inside Select-ActorValue) with ONE closing brace in the common
context; main's block gets an explicit brace and this branch's keeps the
shared one. All four functions stay above the ROGUE_PS_LIB_ONLY seam.
* heartbeat.sh / CLAUDE.md: main's newer prose about the two triggers, with
this branch's corrected actor-cascade and `agent` field descriptions.
* SKILL.md: this branch's resolved-actor reporting AND main's hook-activity
log tail; main's duplicate raw-value echoes dropped, since this branch
reports the cascade's output instead.
tests/test_status_skill_sh.sh stages surface.sh into its fake plugin root — the
skill now reads the shared table as a real install does, and without it the
fixture fell back to the damaged-install literal and reported claude_code for a
Cowork session. tests/test_hook_logs.{sh,ps1} gain the agent-id projection and the
IS_COWORK-beats-entrypoint rows in all three projections, in both twins.
Verified: 11 sh suites and 6 PowerShell suites pass; all shell + all .ps1 parse;
JSON valid; version sync ok for all four plugins; sync-shared-scripts --check
clean; main's own snippet-parse gate passes over all 13 command/skill docs.
tests/test_hook_sh_copilot.sh fails and test_hook_sh_antigravity.sh flakes on a
cleanup race — both reproduced on origin/main untouched, neither caused here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pulls the parent's main-merge (and main's log-shipping feature) up into the stacked child, so #41 is mergeable once #40 lands. hook.sh and hook.ps1 merged cleanly despite main restructuring both — the modal lives at the end of the block branch, main's changes were in logging and surface resolution. Verified the pieces the Cowork gate depends on are still ordered correctly: install-id.sh is sourced (hook.sh:153) before block detection (:187) and the gate call (:202), so ROGUE_INSTALL_AGENT is populated; on the PowerShell side $installAgent now comes from Get-RogueSurfaceAgentId, the shared projection the parent merge introduced, which answers `claude_cowork` — the exact value Test-WantAlert compares against. Had the parent kept main's display-label version of that resolution, the modal would have silently never fired in local Cowork. Only CLAUDE.md conflicted, and only as two disjoint additions: this branch's "The Cowork block modal" section plus main's "The log shipper" section, and one bullet each in "Things that look weird". Both kept. Ordering is deliberate — the Cowork section is a `###` and has to stay ahead of main's `## The log shipper` or it would re-nest as a subsection of the log shipper. No version bump here: the parent carries 1.0.23 -> 1.0.27 for the whole stack (1.0.27, not 1.0.24, because main released v1.0.26 in the meantime and a merged 1.0.24 would read as older than what is published). Verified on the merged tree: all 27 Cowork-modal assertions still pass under both sh and dash, 11 sh suites and 6 PowerShell suites pass (70 hook.ps1 assertions), all shell and .ps1 parse, sync-shared-scripts --check clean, and #41's own diff against its parent is still exactly its 8 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-adds the native block modal that
c31ee5adeleted — for Claude Cowork local sessions only. The CLI and the Claude Desktop app keep getting no modal.Why
c31ee5aremovedsecurity-alert.{sh,ps1}on the grounds that "Claude Desktop now displays hook blocks natively, so the plugin is pure relay". That was correct for the surfaces that existed then, and is still correct for both of them. Claude Cowork was not a surface when that commit landed, and it discards hook-authored text on every documented channel.Measured 2026-08-18, one session per surface, with a probe plugin that recorded its own
uname/whoami/HOME/process ancestry/env from inside the hook process:{"decision":"block","reason":R}{"continue":false,"stopReason":R}systemMessagealongside a blockhookSpecificOutput.additionalContextosascriptmodalFour independent documented channels reach the same dead end, so no JSON shape fixes this.
additionalContextis not a substitute — it reaches the model, which may or may not repeat it. In a local session the OS modal is the only channel that reaches the user.The gate
_rogue_want_alert(hook.sh) /Test-WantAlert(hook.ps1), four tests, cheap ones first:ROGUE_INSTALL_AGENT == "claude_cowork"frominstall-id.shrather than re-deriving the surface. Local Cowork's entrypoint islocal-agent, not a*cowork*value, so entrypoint matching alone files it as the CLI;install-id.shalready handles that, and a second copy would drift from the roster's.CLAUDE_CODE_REMOTEset means the cloud container (Linux,root,HOME=/root, noosascript, noDISPLAY, noDBUS). Excluded explicitly rather than left to fail silently, so~/.rogue/hook.logstays honest.ROGUE_ALERT=0disables;ROGUE_ALERT_EVENTSis a space-separated event allowlist.command -v osascript. The env vars above are undocumented and will move.A declined alert logs
alert_skipped=1 entrypoint=… cowork=… remote=… agent=…; a fired one logsalert_rc=<exit>(TCC denials and osascript failures are otherwise invisible).The relay is untouched. The modal is a side-channel — the server body still goes back verbatim, and every path still exits 0.
Two deviations from the plan, both deliberate
security-alert.shis NOT restored byte-for-byte. The historical version wraps itsosascriptintell application "System Events". A cross-apptellis an Automation request, so macOS attributes it to the host app and prompts "Claude" wants access to control "System Events" on the first block — a consent dialog in front of a security alert, which a user can deny, permanently killing it.plugins/copilotalready learned this under PyCharm andCLAUDE.mddocuments it. Replaced with the baredisplay alert+ top-levelactivateform (needs no permission). Everything else is byte-for-byte, including the bash-only\nconversion — hencebash, notsh, at the call site.install-id.sh's surface resolution instead of re-deriving the Cowork cascade inline, perCLAUDE.md's "resolve host/version/agent in ONE place per plugin".No version bump in this PR. #40 already carries
1.0.23→1.0.24, and the two merge together, so a second bump would ship two releases for one merge. (The plan asked for1.0.25plus a bump toinstall.{sh,ps1}; those installer files carry no version literal anyway — 1.0.24's installer edits were theagent-id fix.)Tests
tests/test_hook_sh.sh— 10 new end-to-end cases: Cowork local fires, cloud skips, CLI/Desktop skip, no-osascriptskips, an allow response fires nothing, a failingosascriptis still fail-open,ROGUE_ALERT_EVENTSnarrows,ROGUE_ALERT=0kills. Every case also asserts the body is relayed verbatim.osascriptstub regression-tests the fd detachment fromdbfd3ee. Verified to fail (30 s wait) when the>/dev/null 2>&1 </dev/nullis removed — without it the subshell holds Claude's read pipe open until the dialog is dismissed, Claude times the hook out, and the block fails open.osascript, so a dev Mac's real/usr/bin/osascriptcan neither defeat the capability case nor pop a dialog on the tester's screen. (It did both on the first draft.)tests/test_hook_ps1.ps1— 13 new cases: the full gate matrix, plus static assertions that the response is relayed before the launch and that the launch stays detached / hidden /-EncodedCommand.tests/test_hooks_json.shpasses unchanged — nohooks.jsonchange; the 27-command polyglot lint is undisturbed.Local run: 40
hook.shassertions pass under bothshanddash; 70hook.ps1assertions pass; all shell scripts parse; all.ps1parse; manifest/version-sync gates pass. (tests/test_hook_ps1_antigravity.ps1has 2 failures — aResolve-Url$script:scope lint in the antigravity plugin — which pre-exist on the base branch and are not in CI's list.)Open question, deliberately left open
PreToolUsepermissionDecision:denydoes render in Cowork cloud (observed twice —CREDENTIAL_THEFT,DANGEROUS_OPERATION, reason visible, no hang). It has not been tested in Cowork local. If tool denials render locally too, the modal should narrow toUserPromptSubmit— the one event with no visible channel — or tool blocks get reported twice. Until that is measured, firing on every block event matches pre-c31ee5abehaviour, andROGUE_ALERT_EVENTS="UserPromptSubmit"narrows it without a release.Verification still owed (needs a real Cowork session)
⛔ Rogue blocked this prompt, log showsoutcome=blockthenalert_rc=0 entrypoint=local-agent.alert_skipped=1 … remote=true, noosascript: command not found.alert_skipped=1 … cowork=unset.Delete this whole path once Cowork renders the reason itself, exactly as it was deleted once Claude Desktop did.
🤖 Generated with Claude Code