Reviewed with an independent reproduction on macOS and Ubuntu 20.04. Fixed five test files that asserted the old literal strings, and switched the reader to `command -p cat` so it also survives hosts without /bin/cat.
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.
Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.
Area: main — git, source-control, providers & integrations. 40 files changed, 1432 insertions(+), 4473 deletions(-).
Co-authored-by: Orca <help@stably.ai>
* Fix hook scripts to drain stdin before any early-exit path
Generated agent hook scripts and missing-script launchers could exit
successfully before consuming the payload written to their stdin,
leaving the writer with a broken pipe (EPIPE/ERROR_BROKEN_PIPE) once
the reader closed early. Capture stdin (or drain it via a shared
epilogue/fast-path guard) before any whole-script success exit across
all POSIX, batch, PowerShell, and Git Bash launcher variants, and add
a cross-agent lifecycle test suite plus a live Electron verification
script to guard the contract going forward.
* Harden hook scripts against unreadable managed scripts and add a Claude/
- Extend the POSIX launcher guard to also require `[ -r ]`, not just `-f`/`-x`,
so an executable-but-unreadable managed script still drains stdin instead of
erroring or silently misbehaving.
- Add a verifier case (`verifyClaudeDevinSkip`) that spins up a local HTTP
server and confirms the Claude hook never forwards a request that Devin
already imported, catching accidental double-forwarding.
- Update installer-utils tests and stdin-lifecycle docs to match the new
readable-file guard and the added verification case.
* Fix hook-launcher verification to derive script paths from the installed
Extract the quoted path from the launcher's `if [ -f '...'` clause instead of
reconstructing it via join(home, ...), so missing/failing-script test cases
can't silently fall through to the real script if the install layout changes.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The POSIX agent-hook script for every curl-based agent inlined the full
event payload via `curl --data-urlencode "payload=${payload}"`. Tool
output can be tens of KB, so the resulting process command line could be
multi-KB — which endpoint security tools (e.g. Microsoft Defender for
Endpoint) flag as an oversized/suspicious command line. That produced a
false-positive detection on Orca's own loopback (127.0.0.1) telemetry POST.
Stream the payload to an mktemp file and post it with
`--data-urlencode "payload@$payload_file"` instead. The urlencoded body on
the wire is byte-identical, so the agent-hook receiver is unchanged; the
payload simply never appears on a process command line. `trap ... EXIT`
removes the temp file on every exit path. Small bounded metadata fields
(paneKey/tabId/worktreeId/env/version) stay inline.
Applied to all curl-based agents: claude, codex, command-code, copilot,
cursor, droid, gemini, grok, antigravity. (amp/hermes/opencode post via
the HTTP request body and were never affected.) The Windows post-command
shares the same latent pattern and is flagged as follow-up.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(codex): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Windows splits raw hook commands on whitespace, so a user profile path
like `C:\Users\Jane Doe` made Codex hooks exit with code 1. Add a
wrapWindowsHookCommand helper that invokes the .cmd through
`cmd.exe /d /c call "..."` and use it in getManagedCommand.
* fix(agent-hooks): wrap Windows hook command in cmd.exe for all agents with raw .cmd path (#6078)
Apply the wrapWindowsHookCommand helper to cursor, command-code, gemini,
grok, and droid, which shared the same raw-scriptPath-on-Windows pattern
as codex. A user profile path with a space (e.g. `C:\Users\Jane Doe`)
used to split at the space and fail with exit code 1.
Agents that already handle spaces correctly are left untouched:
- claude/openclaude (Git Bash + forward slashes)
- copilot (PowerShell with quoted path)
- kimi (Git Bash + forward slashes)
- antigravity (event-specific wrapper .cmd files)
- devin (already wraps via `cmd /d /s /c ""...""`)
Each fixed agent gets a Windows-only test asserting the cmd.exe wrapping
survives spaces in the profile path.
* fix(claude): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Claude Code runs hooks through Git Bash on Windows. The previous
forward-slash trick only works when the path has no spaces — Git Bash
splits `C:/Users/Jane Doe/...` at the space and tries to execute
`C:/Users/Jane` as a command. Use wrapWindowsHookCommand so the .cmd is
invoked through `cmd.exe /d /c call "..."`, which Git Bash treats as one
argument. Applies to both Claude and OpenClaude (shared getManagedCommand).
* Harden Windows agent hook launcher
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Generated agent hook POSTs now use short curl and PowerShell request timeouts so best-effort status hooks cannot hold agent subprocesses open on a stalled local listener.
* feat(cursor): first-class Cursor CLI agent status via ~/.cursor/hooks.json
Give cursor-agent the same hook-driven status pipeline Claude/Codex/Gemini/
OpenCode already use so working/done/permission transitions show the proper
sidebar spinner instead of falling back to title heuristics — cursor-agent
only sets its OSC title to the literal string "Cursor Agent" during a turn,
so title-based detection cannot see working→done transitions on its own.
- New CursorHookService installs a managed shell script and registers it
under ~/.cursor/hooks.json for beforeSubmitPrompt, preToolUse, postToolUse,
postToolUseFailure, beforeShellExecution, beforeMCPExecution, stop, and
afterAgentResponse (the subset that marks turn boundaries and surfaces
in-flight tool context).
- AgentHookServer gains a /hook/cursor route and a normalizeCursorEvent
mapping the camelCase cursor events to working/done/waiting, with tool
previews for preToolUse/shell/MCP and lastAssistantMessage from
afterAgentResponse. stop with status != "completed" surfaces as
interrupted (matches Claude's is_interrupt behavior).
- cursorHookService joins the startup install loop alongside Claude/Codex/
Gemini, and the IPC status handler is exposed via preload.
- 'cursor' added to WellKnownAgentType and to the renderer's
WELL_KNOWN_LABELS so the dashboard prints "Cursor" rather than the raw
'cursor' id.
Verified end-to-end against a real cursor-agent 2026.04.17-787b533 binary:
a mock hook receiver pointed at by ~/.cursor/hooks.json observes
beforeSubmitPrompt → stop for a live turn.
Co-authored-by: Orca <help@stably.ai>
* feat(cursor): wire hook events into sidebar spinner + unread pipeline
The initial hook wiring landed behind AGENT_DASHBOARD_ENABLED, which is
still false. That meant cursor-agent panes lit up no spinner and no
unread indicator — cursor's native OSC title stays literally "Cursor
Agent" across a turn, so title-based detection cannot transition.
Plumb cursor's hook stream into the existing, shipped title-tracker
pipeline (the one Claude/Codex/Pi drive working/idle/unread off) by
synthesizing OSC title sequences in the main-process hook listener:
- `working` → `\x1b]0;⠋ Cursor Agent\x07` (braille prefix → working)
- `waiting` → `\x1b]0;Cursor - action required\x07\x07`
- `done` → `\x1b]0;Cursor ready\x07\x07`
The two trailing BELs on done/waiting are load-bearing: the unread badge
keys off BEL (0x07 outside any OSC), and cursor-agent emits none on its
own. The first BEL is consumed as the OSC terminator; the second fires
the bell detector.
Also treats the bare native "Cursor Agent" title as a no-op in
`detectAgentStatusFromTitle` so cursor's own per-turn re-emissions cannot
stomp our synthesized working state back to idle. `isClaudeAgent`
excludes cursor-bearing braille titles so the Claude prompt-cache timer
doesn't fire for cursor panes.
Scope: the hook server + cursor install run unconditionally now, but
Claude/Codex/Gemini installs stay gated behind AGENT_DASHBOARD_ENABLED,
so only cursor events flow through the pipeline. No dashboard surface
is turned on.
Verified end-to-end in Electron (dev build): launched cursor-agent via
the Cursor menu item, submitted three prompts (including a tool-use
turn reading package.json). Observed tab title flip to "Cursor ready"
on done, and the tab + worktree both transitioned to unread
(unreadTerminalTabs[tabId]=true, worktree.isUnread=true). Parallel
Claude Code pane untouched.
Co-authored-by: Orca <help@stably.ai>
* fix(cursor): animate spinner frames + filter bare native title so the spinner doesn't go solid mid-turn
cursor-agent re-emits its bare "Cursor Agent" OSC title on every internal
redraw, which was stomping the single synthesized "⠋ Cursor Agent" frame
in runtimePaneTitlesByTabId within milliseconds and flipping the sidebar
dot back to solid. Two-part fix:
- Main: drive an 80ms Pi-style braille spinner from the cursor hook
channel, keyed by paneKey, torn down on pty exit via a new
registerPaneKeyTeardownListener hook in ipc/pty.
- Renderer: drop bare "Cursor Agent" titles in pty-transport so cursor's
native re-emissions cannot overwrite the synthesized working/idle
titles.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>