@pushary/agent-hooks 0.84.1 → 0.85.1

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +24 -0
  3. package/data/cursor-plugin/CHANGELOG.md +21 -0
  4. package/data/cursor-plugin/scripts/pushary-gate.mjs +54 -222
  5. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +11 -106
  6. package/data/vscode-plugin/CHANGELOG.md +26 -0
  7. package/data/vscode-plugin/scripts/pushary-gate.mjs +66 -74
  8. package/dist/bin/pushary-bell-hook.js +9 -7
  9. package/dist/bin/pushary-bell.js +12 -11
  10. package/dist/bin/pushary-claude.js +12 -11
  11. package/dist/bin/pushary-clean.js +8 -7
  12. package/dist/bin/pushary-codex-bridge.js +5 -5
  13. package/dist/bin/pushary-codex-hook.js +14 -20
  14. package/dist/bin/pushary-codex.js +3 -3
  15. package/dist/bin/pushary-connect.js +10 -10
  16. package/dist/bin/pushary-daemon.js +6 -6
  17. package/dist/bin/pushary-disconnect.d.ts +1 -0
  18. package/dist/bin/pushary-disconnect.js +125 -0
  19. package/dist/bin/pushary-doctor.js +16 -16
  20. package/dist/bin/pushary-gemini-bridge.js +5 -5
  21. package/dist/bin/pushary-gemini-hook.js +14 -20
  22. package/dist/bin/pushary-hook.js +16 -23
  23. package/dist/bin/pushary-login.js +6 -6
  24. package/dist/bin/pushary-logout.js +6 -6
  25. package/dist/bin/pushary-mode.js +5 -5
  26. package/dist/bin/pushary-notification-hook.js +8 -11
  27. package/dist/bin/pushary-permission-denied-hook.js +14 -12
  28. package/dist/bin/pushary-permission-hook.js +14 -12
  29. package/dist/bin/pushary-post-hook.js +8 -11
  30. package/dist/bin/pushary-prompt-hook.js +8 -11
  31. package/dist/bin/pushary-session-end-hook.js +8 -8
  32. package/dist/bin/pushary-session-start-hook.js +8 -8
  33. package/dist/bin/pushary-setup.js +29 -30
  34. package/dist/bin/pushary-stats.js +4 -4
  35. package/dist/bin/pushary-status.js +7 -7
  36. package/dist/bin/pushary-stop-hook.js +8 -8
  37. package/dist/bin/pushary-stopfailure-hook.js +8 -8
  38. package/dist/bin/pushary-suggestions.js +2 -2
  39. package/dist/bin/pushary-upgrade.js +15 -10
  40. package/dist/bin/pushary-wait.js +5 -5
  41. package/dist/bin/pushary.js +2 -1
  42. package/dist/{chunk-ILB6UOAF.js → chunk-55656H4Q.js} +1 -1
  43. package/dist/chunk-6CUUA7HO.js +59 -0
  44. package/dist/{chunk-6LC4WSCL.js → chunk-BZKTCOZQ.js} +1 -1
  45. package/dist/{chunk-K7CSRGKZ.js → chunk-CIVG746V.js} +1 -1
  46. package/dist/{chunk-FBGBJXVC.js → chunk-DNSIVHIA.js} +1 -1
  47. package/dist/{chunk-437T77TY.js → chunk-ES3FAX5X.js} +1 -1
  48. package/dist/chunk-EZPRY2ZY.js +70 -0
  49. package/dist/{chunk-MQMPG5X4.js → chunk-G3LXA2ZP.js} +207 -10
  50. package/dist/chunk-GM54KC62.js +187 -0
  51. package/dist/{chunk-MGJBC6DD.js → chunk-GOEXZ5QJ.js} +1 -0
  52. package/dist/chunk-HSGEEBBL.js +37 -0
  53. package/dist/{chunk-UXWEE3QI.js → chunk-IGV4KL7B.js} +1 -1
  54. package/dist/{chunk-GCKAWHFP.js → chunk-JDQY47OW.js} +1 -1
  55. package/dist/{chunk-7PZPJSWI.js → chunk-KTKIZEFT.js} +1 -1
  56. package/dist/{chunk-YR5TSPXB.js → chunk-LB6RSCLL.js} +12 -10
  57. package/dist/{chunk-DOXSWT4Q.js → chunk-OZKRF7MS.js} +6 -6
  58. package/dist/{chunk-SEAHGHCE.js → chunk-PHXEADSL.js} +40 -7
  59. package/dist/{chunk-F52SIIDG.js → chunk-QDKJTPAT.js} +1 -1
  60. package/dist/{chunk-J4KFVOCQ.js → chunk-SQOFGPNX.js} +1 -1
  61. package/dist/{chunk-NJ7JT26G.js → chunk-VGADRFG6.js} +1 -1
  62. package/dist/{chunk-HZNOAJGB.js → chunk-VKJUVPTJ.js} +2 -2
  63. package/dist/{chunk-ERYKGH4J.js → chunk-XIR6N66V.js} +1 -1
  64. package/dist/{chunk-CCH775Y6.js → chunk-XLM6COJA.js} +1 -1
  65. package/dist/{chunk-CCCJ6BPW.js → chunk-Y7BALRVB.js} +73 -134
  66. package/dist/chunk-YHG74UFF.js +12 -0
  67. package/dist/src/index.d.ts +7 -22
  68. package/dist/src/index.js +11 -8
  69. package/package.json +6 -3
  70. package/dist/chunk-BLDDXHHA.js +0 -66
  71. package/dist/chunk-UKSQWTBH.js +0 -208
@@ -0,0 +1,70 @@
1
+ import {
2
+ parseVersion
3
+ } from "./chunk-GM54KC62.js";
4
+
5
+ // src/setup/agent-probe.ts
6
+ import { execSync } from "child_process";
7
+ import { readFileSync, realpathSync } from "fs";
8
+
9
+ // src/setup/claude-version.ts
10
+ import { dirname, basename, join } from "path";
11
+ var versionFromInstallPath = (path) => {
12
+ const name = basename(path);
13
+ return /^\d+\.\d+\.\d+$/.test(name) ? parseVersion(name) : null;
14
+ };
15
+ var versionFromManifest = (probe, binPath) => {
16
+ let dir = dirname(binPath);
17
+ for (let depth = 0; depth < 4; depth++) {
18
+ const raw = probe.readFile(join(dir, "package.json"));
19
+ if (raw) {
20
+ try {
21
+ const { version } = JSON.parse(raw);
22
+ if (typeof version === "string") {
23
+ const parsed = parseVersion(version);
24
+ if (parsed) return parsed;
25
+ }
26
+ } catch {
27
+ }
28
+ }
29
+ const parent = dirname(dir);
30
+ if (parent === dir) break;
31
+ dir = parent;
32
+ }
33
+ return null;
34
+ };
35
+ var readClaudeVersion = (probe) => {
36
+ const onPath2 = probe.resolveOnPath();
37
+ if (!onPath2) return null;
38
+ const resolved = probe.realpath(onPath2) ?? onPath2;
39
+ return versionFromInstallPath(resolved) ?? versionFromManifest(probe, resolved);
40
+ };
41
+
42
+ // src/setup/agent-probe.ts
43
+ var onPath = (name) => {
44
+ const command = process.platform === "win32" ? "where" : "which";
45
+ try {
46
+ const found = execSync(`${command} ${name}`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
47
+ return found || null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+ var realpath = (path) => {
53
+ try {
54
+ return realpathSync(path);
55
+ } catch {
56
+ return null;
57
+ }
58
+ };
59
+ var readFile = (path) => {
60
+ try {
61
+ return readFileSync(path, "utf-8");
62
+ } catch {
63
+ return null;
64
+ }
65
+ };
66
+ var detectClaudeVersion = () => readClaudeVersion({ resolveOnPath: () => onPath("claude"), realpath, readFile });
67
+
68
+ export {
69
+ detectClaudeVersion
70
+ };
@@ -65,6 +65,7 @@ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
65
65
  };
66
66
  var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
67
67
  var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
68
+ var HOOK_BATCH_MAX_BYTES = 4 * 1024 * 1024;
68
69
  var REPO_KEY_MAX_LENGTH = 200;
69
70
  var isRepoKeyMatch = (ruleRepoKey, currentRepoKey) => {
70
71
  if (!ruleRepoKey) return true;
@@ -635,6 +636,88 @@ var SECRET_REDACTION_RULES = [
635
636
  var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
636
637
  var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
637
638
  var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
639
+ var AUTHORIZATION_CANONICAL_VERSION = "pushary.authz.v1";
640
+ var AUTHORIZATION_CANONICAL_FIXTURE = [
641
+ {
642
+ subject: { toolName: "refund.create" },
643
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
644
+ action:13:refund.create
645
+ target:0:
646
+ actor:0:
647
+ environment:0:
648
+ externalId:0:`
649
+ },
650
+ {
651
+ subject: {
652
+ toolName: "refund.create",
653
+ toolTarget: "ord_991",
654
+ actor: "user:u_44",
655
+ environment: "production",
656
+ externalId: "cust_7",
657
+ parameters: { currency: "EUR", amount: 4800, expedited: true, rate: 1.5 }
658
+ },
659
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
660
+ action:13:refund.create
661
+ target:7:ord_991
662
+ actor:9:user:u_44
663
+ environment:10:production
664
+ externalId:6:cust_7
665
+ p:13:amount=n:4800
666
+ p:14:currency=s:EUR
667
+ p:16:expedited=b:true
668
+ p:10:rate=n:1.5`
669
+ },
670
+ {
671
+ // Parameter order is not part of the subject. The same facts written in a
672
+ // different order are the same authorization.
673
+ subject: { toolName: "refund.create", parameters: { b: 2, a: 1 } },
674
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
675
+ action:13:refund.create
676
+ target:0:
677
+ actor:0:
678
+ environment:0:
679
+ externalId:0:
680
+ p:5:a=n:1
681
+ p:5:b=n:2`
682
+ },
683
+ {
684
+ // An integral float is the same number as the integer.
685
+ subject: { toolName: "a", parameters: { n: 49 } },
686
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
687
+ action:1:a
688
+ target:0:
689
+ actor:0:
690
+ environment:0:
691
+ externalId:0:
692
+ p:6:n=n:49`
693
+ },
694
+ {
695
+ // Delimiters inside a value cannot forge another subject.
696
+ subject: { toolName: "a", parameters: { "x\ny": "1" } },
697
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
698
+ action:1:a
699
+ target:0:
700
+ actor:0:
701
+ environment:0:
702
+ externalId:0:
703
+ p:7:x
704
+ y=s:1`
705
+ },
706
+ {
707
+ // Every finite number, including the ones another language would render
708
+ // differently. One runtime computes both sides of the comparison.
709
+ subject: { toolName: "a", parameters: { small: 1e-7, big: 1e21 } },
710
+ canonical: `${AUTHORIZATION_CANONICAL_VERSION}
711
+ action:1:a
712
+ target:0:
713
+ actor:0:
714
+ environment:0:
715
+ externalId:0:
716
+ p:11:big=n:1e+21
717
+ p:12:small=n:1e-7`
718
+ }
719
+ ];
720
+ var AUTHORIZATION_PERMIT_TTL_SECONDS = 60 * 60;
638
721
  var classifyDecisionRisk = (toolName, toolTarget) => {
639
722
  const parts = [toolName, toolTarget ?? ""];
640
723
  const matchesAny = (patterns) => parts.some((part) => patterns.some((re) => re.test(part)));
@@ -833,9 +916,127 @@ var isScopeContract = (value) => {
833
916
  const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
834
917
  return strings(c.allowedPaths) && strings(c.offLimitsPaths) && typeof c.doneWhen === "string";
835
918
  };
919
+ var autoApprove = (tool) => ({
920
+ tool,
921
+ timeoutSeconds: 0,
922
+ timeoutAction: "approve",
923
+ mode: "terminal_only",
924
+ pushFirstSeconds: 0
925
+ });
926
+ var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey, sessionId) => {
927
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
928
+ const arg = args[0];
929
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
930
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
931
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
932
+ return "safe_readonly";
933
+ }
934
+ return "policy_timeout";
935
+ };
936
+ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey, sessionId) => {
937
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
938
+ const arg = args[0];
939
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
940
+ let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
941
+ tool: toolName,
942
+ timeoutSeconds: config.defaultTimeoutSeconds,
943
+ timeoutAction: config.defaultTimeoutAction,
944
+ mode: config.defaultMode ?? "push_first",
945
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
946
+ };
947
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
948
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
949
+ base = autoApprove(base.tool);
950
+ }
951
+ const wouldAutoApprove = isAutoApprove(base);
952
+ if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
953
+ base = {
954
+ tool: base.tool,
955
+ timeoutSeconds: config.defaultTimeoutSeconds,
956
+ timeoutAction: "wait",
957
+ mode: "push_first",
958
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
959
+ };
960
+ }
961
+ const effectiveOverride = modeOverride ?? config.modeOverride;
962
+ if (effectiveOverride) {
963
+ return { ...base, mode: effectiveOverride };
964
+ }
965
+ return base;
966
+ };
967
+ var restraintRank = (policy) => {
968
+ if (isAutoDeny(policy)) return 4;
969
+ if (isAutoApprove(policy)) return 0;
970
+ if (policy.mode === "push_only") return 3;
971
+ return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
972
+ };
973
+ var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
974
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey, sessionId) => {
975
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey, sessionId);
976
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey, sessionId)).reduce(strictestPolicy);
977
+ };
978
+ var isModeStateDegraded = (state) => state?.degraded === true;
979
+ var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
980
+ var strictestScopeVerdict = (scope, paths) => {
981
+ if (paths.length === 0) return evaluateScope(scope, void 0);
982
+ let worst = { outcome: "in_scope" };
983
+ for (const path of paths) {
984
+ const verdict = evaluateScope(scope, path);
985
+ if (verdict.outcome === "off_limits") return verdict;
986
+ if (verdict.outcome === "out_of_scope") worst = verdict;
987
+ }
988
+ return worst;
989
+ };
990
+ var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey, sessionId) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey, sessionId) === "safe_readonly";
991
+ var resolveGate = (input) => {
992
+ const { modeState, config, toolName, toolInputs, cwd, repoKey, sessionId } = input;
993
+ if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
994
+ const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId);
995
+ const verdict = verdictForPolicy(policy);
996
+ const autoApproves = verdict === "allow";
997
+ const autoDenies = verdict === "deny";
998
+ if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
999
+ if (isModeStateDegraded(modeState)) {
1000
+ if (autoApproves) {
1001
+ return survivesDegraded(config, toolName, toolInputs, cwd, repoKey, sessionId) ? { kind: "allow", policy, source: "allowlist" } : { kind: "defer", policy };
1002
+ }
1003
+ return { kind: "gate", policy };
1004
+ }
1005
+ const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
1006
+ if (autoApproves) {
1007
+ if (!scopeRequiresHuman(scopeVerdict)) {
1008
+ return {
1009
+ kind: "allow",
1010
+ policy,
1011
+ source: toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey, sessionId) === "safe_readonly" ? "allowlist" : "policy"
1012
+ };
1013
+ }
1014
+ }
1015
+ const scopeReason = scopeChangeReason(scopeVerdict);
1016
+ return {
1017
+ kind: "gate",
1018
+ policy,
1019
+ scopeReason,
1020
+ scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
1021
+ };
1022
+ };
1023
+ var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
1024
+ var modeStateFromResponse = (data) => {
1025
+ const mode = data.override?.mode;
1026
+ return {
1027
+ mode: isApprovalMode(mode) ? mode : null,
1028
+ kill: data.kill === true,
1029
+ policyVersion: toPolicyVersion(data.policyVersion),
1030
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null,
1031
+ trainingConsent: data.trainingConsent === true,
1032
+ // Validated rather than cast: a malformed contract must read as no contract,
1033
+ // never as one that silently matches nothing.
1034
+ scope: isScopeContract(data.scope) ? data.scope : null,
1035
+ degraded: false
1036
+ };
1037
+ };
836
1038
 
837
1039
  export {
838
- isApprovalMode,
839
1040
  parseAgentQuestionAnswers,
840
1041
  normalizeInstallSource,
841
1042
  HOOK_BUDGETS,
@@ -844,21 +1045,17 @@ export {
844
1045
  effectiveWaitSeconds,
845
1046
  normalizeRepoRemote,
846
1047
  localRepoKey,
847
- policyArgForms,
848
1048
  isSafeReadOnlyCommand,
849
1049
  isAutoApprove,
850
- isAutoDeny,
851
- verdictForPolicy,
852
- selectGoverningRule,
853
1050
  isValidApiKey,
854
1051
  ACTION_BODY_MAX,
855
1052
  DECISION_LINE_MAX,
856
1053
  redactSecrets,
857
1054
  redactSecretsDeep,
858
- classifyDecisionRisk,
859
1055
  buildDecisionEpisodeFeatures,
860
- evaluateScope,
861
- scopeRequiresHuman,
862
- scopeChangeReason,
863
- isScopeContract
1056
+ resolveAutoResolveOrigin,
1057
+ resolvePolicy,
1058
+ KILL_REASON,
1059
+ resolveGate,
1060
+ modeStateFromResponse
864
1061
  };
@@ -0,0 +1,187 @@
1
+ import {
2
+ guardBinary
3
+ } from "./chunk-HSGEEBBL.js";
4
+ import {
5
+ HOOK_BUDGETS
6
+ } from "./chunk-G3LXA2ZP.js";
7
+
8
+ // src/claude-config.ts
9
+ import { join } from "path";
10
+
11
+ // src/agent-version.ts
12
+ var parseVersion = (raw) => {
13
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
14
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
15
+ };
16
+ var compareVersion = (a, b) => {
17
+ for (let index = 0; index < 3; index++) {
18
+ if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
19
+ }
20
+ return 0;
21
+ };
22
+ var atLeast = (version, floor) => compareVersion(version, floor) >= 0;
23
+
24
+ // src/claude-tools.ts
25
+ var PRETOOLUSE_GATED_TOOLS = [
26
+ "Bash",
27
+ "Write",
28
+ "Edit",
29
+ "MultiEdit",
30
+ "NotebookEdit",
31
+ "PowerShell",
32
+ "Monitor"
33
+ ];
34
+ var POSTTOOLUSE_MIRRORED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "TodoWrite"];
35
+ var PRETOOLUSE_HANDLED_TOOLS = /* @__PURE__ */ new Set([...PRETOOLUSE_GATED_TOOLS, "AskUserQuestion"]);
36
+
37
+ // src/claude-events.ts
38
+ var GATED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "AskUserQuestion"].join("|");
39
+ var MIRRORED_TOOLS = [...PRETOOLUSE_GATED_TOOLS, "TodoWrite"].join("|");
40
+ var ESTABLISHED = [1, 0, 0];
41
+ var OBSERVED = [2, 0, 0];
42
+ var CLAUDE_HOOK_EVENTS = [
43
+ { event: "PreToolUse", binary: "pushary-hook", matcher: GATED_TOOLS, timeout: HOOK_BUDGETS.claude.budgetSeconds, delivery: "approval", since: ESTABLISHED },
44
+ { event: "PostToolUse", binary: "pushary-post-hook", matcher: MIRRORED_TOOLS, timeout: 10, delivery: "telemetry", since: ESTABLISHED },
45
+ { event: "Stop", binary: "pushary-stop-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
46
+ { event: "UserPromptSubmit", binary: "pushary-prompt-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
47
+ { event: "Notification", binary: "pushary-notification-hook", matcher: "permission_prompt|idle_prompt|agent_needs_input|agent_completed", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
48
+ { event: "SessionStart", binary: "pushary-session-start-hook", matcher: "startup|resume|clear", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
49
+ { event: "SessionEnd", binary: "pushary-session-end-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
50
+ { event: "StopFailure", binary: "pushary-stopfailure-hook", timeout: 10, delivery: "telemetry", since: ESTABLISHED },
51
+ { event: "PermissionRequest", binary: "pushary-permission-hook", timeout: HOOK_BUDGETS.claude.budgetSeconds, delivery: "approval", since: ESTABLISHED },
52
+ { event: "PermissionDenied", binary: "pushary-permission-denied-hook", timeout: 60, delivery: "telemetry", since: ESTABLISHED },
53
+ // Observability only. Each carries session shape the island cannot get any other
54
+ // way, and none of them decides anything.
55
+ { event: "SubagentStart", binary: "pushary-session-start-hook", timeout: 10, delivery: "telemetry", since: OBSERVED },
56
+ { event: "SubagentStop", binary: "pushary-stop-hook", timeout: 10, delivery: "telemetry", since: OBSERVED },
57
+ { event: "PreCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, delivery: "telemetry", since: OBSERVED },
58
+ { event: "PostCompact", binary: "pushary-post-hook", matcher: "manual|auto", timeout: 10, delivery: "telemetry", since: OBSERVED },
59
+ { event: "TeammateIdle", binary: "pushary-notification-hook", timeout: 10, delivery: "telemetry", since: OBSERVED }
60
+ ];
61
+ var supportedClaudeEvents = (version) => CLAUDE_HOOK_EVENTS.filter((hook) => version ? atLeast(version, hook.since) : hook.since === ESTABLISHED);
62
+
63
+ // src/claude-config.ts
64
+ var PUSHARY_MCP_URL = "https://pushary.com/api/mcp/mcp";
65
+ var PUSHARY_PERMISSION_RULE = "mcp__pushary__*";
66
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
67
+ var ensureRecord = (target, key) => {
68
+ const existing = asRecord(target[key]);
69
+ if (existing) return existing;
70
+ const created = {};
71
+ target[key] = created;
72
+ return created;
73
+ };
74
+ var isPusharyPermission = (rule) => typeof rule === "string" && (rule.includes("pushary") || rule.includes("MCP(pushary"));
75
+ var HOOK_BINARIES = new Set(CLAUDE_HOOK_EVENTS.map((hook) => hook.binary));
76
+ var namesPusharyBinary = (command) => command.split(/[\s/\\"']+/).some((segment) => HOOK_BINARIES.has(segment.replace(/\.(cmd|exe)$/i, "")));
77
+ var isPusharyHook = (entry2) => {
78
+ const hooks = asRecord(entry2)?.hooks;
79
+ if (!Array.isArray(hooks)) return false;
80
+ return hooks.some((hook) => namesPusharyBinary(String(asRecord(hook)?.command ?? "")));
81
+ };
82
+ var addClaudeMcpServer = (config, apiKey) => {
83
+ const mcpServers = ensureRecord(config, "mcpServers");
84
+ mcpServers.pushary = {
85
+ type: "http",
86
+ url: PUSHARY_MCP_URL,
87
+ headers: { Authorization: `Bearer ${apiKey}` }
88
+ };
89
+ };
90
+ var removeClaudeMcpServers = (config) => {
91
+ let changed = false;
92
+ const removeFrom = (target) => {
93
+ const mcpServers = asRecord(target.mcpServers);
94
+ if (!mcpServers?.pushary) return;
95
+ delete mcpServers.pushary;
96
+ if (Object.keys(mcpServers).length === 0) delete target.mcpServers;
97
+ changed = true;
98
+ };
99
+ removeFrom(config);
100
+ const projects = asRecord(config.projects);
101
+ if (projects) {
102
+ for (const project of Object.values(projects)) {
103
+ const projectConfig = asRecord(project);
104
+ if (projectConfig) removeFrom(projectConfig);
105
+ }
106
+ }
107
+ return changed;
108
+ };
109
+ var addPusharyToolPermissions = (settings) => {
110
+ const permissions = ensureRecord(settings, "permissions");
111
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
112
+ const filtered = allow.filter((rule) => !isPusharyPermission(rule));
113
+ if (!filtered.includes(PUSHARY_PERMISSION_RULE)) {
114
+ filtered.push(PUSHARY_PERMISSION_RULE);
115
+ }
116
+ permissions.allow = filtered;
117
+ };
118
+ var addPusharyHooks = (settings, binDir, version = null) => {
119
+ const resolve = (name) => guardBinary(binDir ? join(binDir, name) : name);
120
+ const hooks = ensureRecord(settings, "hooks");
121
+ const supported = new Map(supportedClaudeEvents(version).map((hook) => [hook.event, hook]));
122
+ for (const known of CLAUDE_HOOK_EVENTS) {
123
+ const current = hooks[known.event];
124
+ const others = (Array.isArray(current) ? current : []).filter((item) => !isPusharyHook(item));
125
+ const definition = supported.get(known.event);
126
+ if (definition) {
127
+ hooks[known.event] = [...others, entry(definition, resolve)];
128
+ } else if (others.length > 0) {
129
+ hooks[known.event] = others;
130
+ } else {
131
+ delete hooks[known.event];
132
+ }
133
+ }
134
+ };
135
+ var entry = (definition, resolve) => ({
136
+ ...definition.matcher ? { matcher: definition.matcher } : {},
137
+ hooks: [{
138
+ type: "command",
139
+ command: resolve(definition.binary),
140
+ timeout: definition.timeout
141
+ }]
142
+ });
143
+ var removePusharySettings = (settings) => {
144
+ let changed = removeClaudeMcpServers(settings);
145
+ const permissions = asRecord(settings.permissions);
146
+ if (permissions && Array.isArray(permissions.allow)) {
147
+ const filtered = permissions.allow.filter((rule) => !isPusharyPermission(rule));
148
+ if (filtered.length !== permissions.allow.length) {
149
+ if (filtered.length === 0) {
150
+ delete permissions.allow;
151
+ } else {
152
+ permissions.allow = filtered;
153
+ }
154
+ if (Object.keys(permissions).length === 0) delete settings.permissions;
155
+ changed = true;
156
+ }
157
+ }
158
+ const hooks = asRecord(settings.hooks);
159
+ if (hooks) {
160
+ for (const { event: key } of CLAUDE_HOOK_EVENTS) {
161
+ const entries = hooks[key];
162
+ if (!Array.isArray(entries)) continue;
163
+ const filtered = entries.filter((entry2) => !isPusharyHook(entry2));
164
+ if (filtered.length !== entries.length) {
165
+ if (filtered.length === 0) {
166
+ delete hooks[key];
167
+ } else {
168
+ hooks[key] = filtered;
169
+ }
170
+ changed = true;
171
+ }
172
+ }
173
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
174
+ }
175
+ return changed;
176
+ };
177
+
178
+ export {
179
+ parseVersion,
180
+ compareVersion,
181
+ PRETOOLUSE_HANDLED_TOOLS,
182
+ addClaudeMcpServer,
183
+ removeClaudeMcpServers,
184
+ addPusharyToolPermissions,
185
+ addPusharyHooks,
186
+ removePusharySettings
187
+ };
@@ -53,6 +53,7 @@ var COMMAND_LIST = [
53
53
  ]
54
54
  },
55
55
  { name: "doctor", module: "pushary-doctor", summary: "Verify your Pushary installation is working" },
56
+ { name: "disconnect", module: "pushary-disconnect", summary: "Turn off one agent (claude|codex|gemini|cursor)" },
56
57
  { name: "clean", module: "pushary-clean", summary: "Remove all Pushary configuration (--yes for non-interactive)" },
57
58
  { name: "mode", module: "pushary-mode", summary: "Switch approval mode (push_only, push_first, terminal_only)" },
58
59
  { name: "wait", module: "pushary-wait", summary: 'Show or set the "wait for your phone" ladder (pushary wait 45)' },
@@ -0,0 +1,37 @@
1
+ import {
2
+ quoteIfNeeded
3
+ } from "./chunk-VT4IVERX.js";
4
+
5
+ // src/hook-command.ts
6
+ var HOOK_SHELL = "/bin/sh -c";
7
+ var LOCATE = 'B=$(command -v "$0") || exit 0; [ -f "$B" ] || exit 0; [ -x "$B" ] || exit 0';
8
+ var NODE_PRESENT = "command -v node >/dev/null 2>&1 || exit 0";
9
+ var HOOK_GUARD_SCRIPTS = {
10
+ native_binary: `${LOCATE}; exec "$B"`,
11
+ node_binary: `${LOCATE}; ${NODE_PRESENT}; exec "$B"`,
12
+ node_script: `[ -f "$0" ] || exit 0; ${NODE_PRESENT}; exec node "$0"`
13
+ };
14
+ var BINARY_GUARDS = {
15
+ native: "native_binary",
16
+ node: "node_binary"
17
+ };
18
+ var guardKindOf = (target) => {
19
+ switch (target.kind) {
20
+ case "node":
21
+ return "node_script";
22
+ case "binary":
23
+ return BINARY_GUARDS[target.runtime];
24
+ }
25
+ };
26
+ var guardHookCommand = (target, platform = process.platform) => {
27
+ const path = quoteIfNeeded(target.kind === "node" ? target.script : target.path);
28
+ if (platform === "win32") return target.kind === "node" ? `node ${path}` : path;
29
+ return `${HOOK_SHELL} '${HOOK_GUARD_SCRIPTS[guardKindOf(target)]}' ${path}`;
30
+ };
31
+ var guardBinary = (path, platform) => guardHookCommand({ kind: "binary", path, runtime: "node" }, platform);
32
+ var guardNodeScript = (script, platform) => guardHookCommand({ kind: "node", script }, platform);
33
+
34
+ export {
35
+ guardBinary,
36
+ guardNodeScript
37
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  codexHomeFrom
3
- } from "./chunk-SEAHGHCE.js";
3
+ } from "./chunk-PHXEADSL.js";
4
4
 
5
5
  // src/vscode-config.ts
6
6
  import { homedir } from "os";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2UMNXADU.js";
4
4
  import {
5
5
  redactSecrets
6
- } from "./chunk-MQMPG5X4.js";
6
+ } from "./chunk-G3LXA2ZP.js";
7
7
 
8
8
  // src/crypto.ts
9
9
  import nacl from "tweetnacl";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-K7CSRGKZ.js";
3
+ } from "./chunk-CIVG746V.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
@@ -1,15 +1,14 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-UKSQWTBH.js";
3
+ } from "./chunk-GM54KC62.js";
4
4
  import {
5
5
  isGatingMoment,
6
6
  recordKeylessMoment
7
- } from "./chunk-NJ7JT26G.js";
7
+ } from "./chunk-VGADRFG6.js";
8
8
  import {
9
9
  denyReasonFrom,
10
- isDeferAnswer,
11
- resolveGate
12
- } from "./chunk-BLDDXHHA.js";
10
+ isDeferAnswer
11
+ } from "./chunk-YHG74UFF.js";
13
12
  import {
14
13
  DEFAULT_SESSION,
15
14
  askUser,
@@ -29,7 +28,7 @@ import {
29
28
  sendNotification,
30
29
  throttlePass,
31
30
  waitForAnswer
32
- } from "./chunk-CCCJ6BPW.js";
31
+ } from "./chunk-Y7BALRVB.js";
33
32
  import {
34
33
  getMachineId
35
34
  } from "./chunk-RN3NOEJF.js";
@@ -42,8 +41,9 @@ import {
42
41
  effectiveWaitSeconds,
43
42
  hookWaitClamped,
44
43
  hookWaitDeadline,
45
- parseAgentQuestionAnswers
46
- } from "./chunk-MQMPG5X4.js";
44
+ parseAgentQuestionAnswers,
45
+ resolveGate
46
+ } from "./chunk-G3LXA2ZP.js";
47
47
 
48
48
  // src/decision-episode.ts
49
49
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -513,14 +513,16 @@ var handlePreToolUse = async (input) => {
513
513
  if (input.tool_name === "AskUserQuestion") {
514
514
  return handleAskUserQuestion(apiKey, input);
515
515
  }
516
+ if (verdict.kind === "deny") return deny(verdict.reason);
516
517
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
517
518
  return void 0;
518
519
  }
519
520
  switch (verdict.kind) {
520
521
  case "allow":
521
522
  return allow();
522
- case "deny":
523
- return deny(verdict.reason);
523
+ // No `deny` case: it is handled above the defer, and the compiler enforces
524
+ // that it stays there. Restoring one here would type-error as unreachable.
525
+ //
524
526
  // State we could not establish, with an auto-approve on the table. No
525
527
  // opinion, so Claude Code's own flow runs as if the hook were absent.
526
528
  case "defer":
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-MQMPG5X4.js";
3
+ } from "./chunk-G3LXA2ZP.js";
4
4
 
5
5
  // src/gemini-config.ts
6
6
  var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
7
7
  var GEMINI_MCP_URL = "https://pushary.com/api/mcp/mcp";
8
8
  var GEMINI_HOOK_EVENTS = [
9
- { event: "BeforeTool", matcher: "run_shell_command|write_file|replace", timeoutMs: HOOK_BUDGETS.gemini.budgetSeconds * 1e3 },
10
- { event: "AfterTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 1e4 },
11
- { event: "BeforeAgent", timeoutMs: 1e4 },
12
- { event: "SessionStart", timeoutMs: 1e4 },
13
- { event: "SessionEnd", timeoutMs: 1e4 }
9
+ { event: "BeforeTool", matcher: "run_shell_command|write_file|replace", delivery: "approval", timeoutMs: HOOK_BUDGETS.gemini.budgetSeconds * 1e3 },
10
+ { event: "AfterTool", matcher: "run_shell_command|write_file|replace", delivery: "telemetry", timeoutMs: 1e4 },
11
+ { event: "BeforeAgent", delivery: "telemetry", timeoutMs: 1e4 },
12
+ { event: "SessionStart", delivery: "telemetry", timeoutMs: 1e4 },
13
+ { event: "SessionEnd", delivery: "telemetry", timeoutMs: 1e4 }
14
14
  ];
15
15
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
16
16
  var ensureRecord = (target, key) => {