@pushary/agent-hooks 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -324,7 +324,14 @@ const main = async () => {
324
324
  diag('no input on stdin. Cursor did not pipe the command to this hook (a known Cursor issue on Windows). Handing off to Cursor\'s own prompt; the push approval cannot run without the command.')
325
325
  return respond(ask())
326
326
  }
327
- input = JSON.parse(raw)
327
+ // Windows: Cursor prepends a BOM/encoding prefix to the hook's stdin, so raw
328
+ // arrives as e.g. "���{...}" and a bare JSON.parse throws,
329
+ // silently dropping us to "ask" with no push (the script works when run
330
+ // manually because there is no BOM). The payload is always a JSON object, so
331
+ // parse from the first "{" — robust to a BOM or whatever prefix bytes Cursor
332
+ // emits. Verified on Windows + Cursor 3.8.11.
333
+ const jsonStart = raw.indexOf('{')
334
+ input = JSON.parse(jsonStart > 0 ? raw.slice(jsonStart) : raw)
328
335
  } catch {
329
336
  diag('stdin was not valid JSON (often empty or corrupted, a known Cursor Windows stdin issue). Handing off to Cursor\'s own prompt.')
330
337
  return respond(ask())
@@ -189,8 +189,18 @@ var main = async () => {
189
189
  }
190
190
  const cursorPluginDir = join(homedir(), ".cursor", "plugins", "local", "pushary");
191
191
  if (existsSync(cursorPluginDir)) {
192
- const cursorGate = join(cursorPluginDir, "hooks", "hooks.json");
193
- check(existsSync(cursorGate), "Cursor: permission gate installed", existsSync(cursorGate) ? "beforeShellExecution gate" : "missing hooks.json, re-run setup");
192
+ const cursorUserHooks = join(homedir(), ".cursor", "hooks.json");
193
+ const userHooks = readJson(cursorUserHooks);
194
+ const gateEntry = userHooks.hooks?.beforeShellExecution?.find(
195
+ (h) => String(h.command ?? "").includes("pushary-gate")
196
+ );
197
+ if (!gateEntry) {
198
+ check(false, "Cursor: permission gate registered", "no Pushary gate in ~/.cursor/hooks.json \u2014 re-run setup (a plugin-only hooks.json is never read by Cursor)");
199
+ } else {
200
+ const scriptPath = String(gateEntry.command).match(/"([^"]+)"/)?.[1] ?? "";
201
+ const resolves = scriptPath ? existsSync(scriptPath) : false;
202
+ check(resolves, "Cursor: permission gate registered", resolves ? `~/.cursor/hooks.json \u2192 ${scriptPath}` : `gate script not found: ${scriptPath || gateEntry.command} \u2014 re-run setup`);
203
+ }
194
204
  const cursorMcp = readJson(join(cursorPluginDir, "mcp.json"));
195
205
  const cursorServers = cursorMcp?.mcpServers ?? {};
196
206
  const cursorAuth = cursorServers.pushary?.headers?.Authorization;
@@ -296,6 +296,7 @@ var connectViaAppPairing = async () => {
296
296
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
297
297
  var CLAUDE_JSON = join(homedir(), ".claude.json");
298
298
  var CURSOR_PLUGIN_DIR = join(homedir(), ".cursor", "plugins", "local", "pushary");
299
+ var CURSOR_USER_HOOKS = join(homedir(), ".cursor", "hooks.json");
299
300
  var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
300
301
  var CODEX_HOME = process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
301
302
  var CODEX_SKILL_DIR = join(CODEX_HOME, "skills", "pushary");
@@ -697,6 +698,37 @@ var resolveBundledPlugin = () => {
697
698
  ];
698
699
  return candidates.find((p) => existsSync(join(p, ".cursor-plugin", "plugin.json"))) ?? null;
699
700
  };
701
+ var installCursorUserHooks = (gateScript) => {
702
+ const template = readJson(join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
703
+ if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
704
+ const entry = { ...template, command: `node "${gateScript}"` };
705
+ let userHooks = {};
706
+ if (existsSync(CURSOR_USER_HOOKS)) {
707
+ try {
708
+ userHooks = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
709
+ } catch {
710
+ try {
711
+ cpSync(CURSOR_USER_HOOKS, `${CURSOR_USER_HOOKS}.bak`);
712
+ } catch {
713
+ }
714
+ userHooks = {};
715
+ }
716
+ }
717
+ const hooks = userHooks.hooks ?? {};
718
+ const existing = Array.isArray(hooks.beforeShellExecution) ? hooks.beforeShellExecution : [];
719
+ const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
720
+ hooks.beforeShellExecution = [...others, entry];
721
+ writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
722
+ };
723
+ var neutralizePluginGate = () => {
724
+ const path = join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
725
+ if (!existsSync(path)) return;
726
+ const data = readJson(path);
727
+ if (data.hooks && "beforeShellExecution" in data.hooks) {
728
+ delete data.hooks.beforeShellExecution;
729
+ writeJson(path, { version: data.version ?? 1, hooks: data.hooks });
730
+ }
731
+ };
700
732
  var setupCursor = async (apiKey) => {
701
733
  console.log(`
702
734
  ${bold2("Setting up Cursor")}
@@ -720,12 +752,16 @@ var setupCursor = async (apiKey) => {
720
752
  writeJson(mcpPath, mcp);
721
753
  }
722
754
  });
755
+ await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
756
+ installCursorUserHooks(join(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
757
+ neutralizePluginGate();
758
+ });
723
759
  console.log();
724
760
  console.log(` ${dim2("What this configured:")}`);
725
- console.log(` ${dim2("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary`);
726
- console.log(` ${dim2("\u2022")} MCP tools, the always-on rule, the skill, and the permission gate`);
761
+ console.log(` ${dim2("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary (MCP tools, rule, skill)`);
762
+ console.log(` ${dim2("\u2022")} Permission gate registered once in ~/.cursor/hooks.json`);
727
763
  console.log(` ${dim2("\u2022")} Risky shell commands route to push approval before they run`);
728
- console.log(` ${dim2("\u2022")} Restart Cursor (or run Developer: Reload Window) to load it`);
764
+ console.log(` ${dim2("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
729
765
  };
730
766
  var saveApiKey = async (apiKey) => {
731
767
  await spinner("Saving your API key", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "author": "Pushary <business@pushary.com>",
6
6
  "homepage": "https://pushary.com",