You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Inline esbuild problem matcher** (no extra extension needed).
629
629
-**`erasableSyntaxOnly`** in webview forbids `enum` — use `as const` objects.
630
630
-**Server always starts** regardless of hooks toggle. Only hook installation is gated by the setting.
631
-
- **Consent before any FIRST `~/.claude/settings.json` write**, one shared `hooksConsentGiven` flag across surfaces. **Exactly one population is prompted: the one with nothing of ours installed.** With our hooks already present but no recorded consent (a pre-consent version installed them silently), consent is granted and the install runs with **no prompt at all** — deliberate zero friction, since that install only ever removes events, never adds them. The accepted cost: consent is read off artifacts our own pre-consent code wrote, and afterwards the **only** removal route is the Settings toggle the disclosure names (pinned in `consent.spec.ts`). The branch is chosen by `areHooksInstalled` = **ANY of our commands on ANY event** — an all-or-nothing reading made a partial install look like "nothing installed", so live hooks were presented as absent and "Don't Ask Again" persisted hooks-off without removing them. The first-run prompt discloses scope, where payloads go, and how to undo (`consentCopy.ts`); VS Code renders it as a **non-modal notification** (`Install Hooks` / `Not Now` / `Don't Ask Again`) carrying the full disclosure in its message — a notification with buttons is permanently expanded with no line clamp and truncates only at 1000 chars, pinned by `consentCopy.test.ts`; standalone asks `[Y/n/never]` on a TTY and installs nothing without one. An unanswered prompt auto-hides to the bell, writes nothing, and asks again next startup.
631
+
- **Consent before any FIRST `~/.claude/settings.json` write**, one shared `hooksConsentGiven` flag across surfaces. **Exactly one population is prompted: the one with nothing of ours installed.** With our hooks already present but no recorded consent (a pre-consent version installed them silently), consent is granted and the install runs with **no prompt at all** — deliberate zero friction, since that install only ever removes events, never adds them. The accepted cost: consent is read off artifacts our own pre-consent code wrote, and afterwards the **only** removal route is the Settings toggle the disclosure names (pinned in `consent.spec.ts`). The branch is chosen by `areHooksInstalled` = **ANY of our commands on ANY event** — an all-or-nothing reading made a partial install look like "nothing installed", so live hooks were presented as absent and "Don't Ask Again" persisted hooks-off without removing them. The first-run prompt discloses scope, where payloads go, and how to undo (`consentCopy.ts`); VS Code renders it as a **blocking modal** (`Install Hooks` / `Not Now` / `Don't Ask Again`) — headline as the message, `CONSENT_DISCLOSURE` as `detail`, the slot VS Code renders only for a modal — because this consent must be unmissable, not merely available; standalone asks `[Y/n/never]` on a TTY and installs nothing without one. **Exactly three buttons**: `Not Now` is passed as a `MessageItem` with `isCloseAffordance`, so VS Code uses it AS the cancel affordance instead of synthesizing a fourth `Cancel` that did precisely what `Not Now` did. The overloads are homogeneous (`T extends string` | `T extends MessageItem`), so marking one item forces all three, and the outcome routing compares by object IDENTITY — a string comparison against a `MessageItem` return is the silent failure this shape rules out. **Only `Don't Ask Again` writes anything on a decline**: `Not Now`, Escape, and the close `x` all write nothing and ask again next startup. Escape/close no longer resolve to `undefined` — VS Code routes them through the dialog's `cancelId` to the close-affordance ITEM — so the handler is fail-closed on identity: only an exact match writes, and every other value (both dismissal forms included) falls through to the no-write path. The e2e specs can see the modal only because `launch.ts` seeds `window.dialogStyle: custom` (a native dialog is invisible to Playwright), and `arrangeReviewLayout` bails out while one is open (the blocking overlay eats the sash drag) with `openSettingsModal` repairing the layout on demand.
632
632
-**`hooksStatus` is install state, `hooksEnabled` is preference.** The Settings checkbox binds to `hooksInstalled` and toggles the _displayed_ state with no optimistic local update, so it can't read "on" over an untouched settings.json and lands correct rather than flickering when an install fails. Every failure path re-derives and broadcasts the truth (standalone via `clientMessageHandler`, VS Code via `reportHooksStatus`). The hook script is copied BEFORE the entries are written; a failed copy aborts the install (entries pointing at a missing script spawn a dead `node` per event). **Hooks-off is persisted only AFTER a successful uninstall** — flipping it first strands the user: the entries keep firing while the persisted preference makes the next start skip the gate entirely.
633
633
-**Never rewrite a shape we did not author.** The unparseable-file abort generalizes: a non-object `hooks`, a non-array `hooks.<Event>`, and junk entries inside an event array are all refused or passed through, never replaced. An array `hooks` was the sharp case — string keys assigned onto it vanish from `JSON.stringify`, so the write committed and reported `installed: true` over a file with no hooks in it. Emptied event keys are deleted only when _our_ removal emptied them. **Internal sentinels must not be values user JSON can hold**: `null` marked "this entry is now empty", so a user-authored `null` inside a hooks array was silently deleted (a file with no Pixel Agents command anywhere came back rewritten and logged as "Hooks removed") — it is a `Symbol` now.
634
634
- **Hook identity is anchored at both ends, not a substring.** `includes('claude-hook.js') && includes('.pixel-agents')` claimed — and `uninstallHooks` then DELETED — a `.backup` copy of our script, a shell comment naming our path, a wrapper passing it as an argument, `/opt/evil.pixel-agents/hooks/claude-hook.js`, and `my-pixel-agents-hook.js`. Ours = the `/.pixel-agents/hooks/claude-hook.js` suffix, ending the command's FIRST token, matched **case-insensitively** (the token is normalized to lower case). Case-sensitive matching is what shipped, and on the case-insensitive volumes this runs on (macOS, Windows) a differently-cased path is the SAME INODE as our script and genuinely firing: reinstall appended a duplicate and uninstall left the cased entry as an orphan our own `areHooksInstalled` could no longer see — a live hook with no removal route. The folding is unconditional (no filesystem case-sensitivity probe), so the accepted trade is a Linux-only false positive that is **not** a mere dedup: on a case-sensitive volume `~/.Pixel-Agents/hooks/claude-hook.js` is a genuinely DIFFERENT file, we classify it as ours, and uninstall **deletes** it (`claudeHookInstaller.test.ts` pins that removal). Nothing creates that path, and the trade is deliberate — the alternative is a guaranteed unremovable live hook on the two platforms this actually ships to. A symlink alias to our script is deliberately _not_ recognized — the cost is one duplicate entry, versus deleting a stranger's hook if we resolved paths.
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -108,7 +108,7 @@ pixel-agents --help
108
108
109
109
The default bind address is `127.0.0.1`. Binding to `0.0.0.0` exposes the UI and WebSocket to the local network; do this only on a trusted network.
110
110
111
-
Open the URL the CLI prints — it carries a `?token=` for this session. Any browser can watch the office without it, but installing or removing hooks (which edits your agent tool's own settings file) is only offered to a session that has the token, so an untokened client on the network cannot approve it. Open the bare address instead and the hooks toggle in Settings is refused, and reports the actual install state rather than appearing to work.
111
+
Open the URL the CLI prints - it carries a `?token=` for this session. Any browser can watch the office without it, but installing or removing hooks (which edits your agent tool's own settings file, like the `~/.claude/settings.json`) is only offered to a session that has the token, so an untokened client on the network cannot approve it. Open the bare address instead and the hooks toggle in Settings is refused, and reports the actual install state rather than appearing to work.
112
112
113
113
Treat that URL as a secret: the token is a bearer capability, not proof of being local. Whoever holds it can approve the hook install from anywhere the server is reachable — so don't paste the URL into a shared channel, and note that it also lands in your browser history and (unredacted) in the server's own request log.
0 commit comments