@pushary/agent-hooks 0.26.0 → 0.28.0

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.
@@ -0,0 +1,322 @@
1
+ // ../../../../../packages/contracts/src/index.ts
2
+ var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
+ var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var HOOK_BUDGETS = {
5
+ claude: { budgetSeconds: 120, guardSeconds: 10 },
6
+ codex: { budgetSeconds: 180, guardSeconds: 10 },
7
+ gemini: { budgetSeconds: 180, guardSeconds: 10 },
8
+ // The Cursor gate is a dependency-free .mjs that cannot import this module; it
9
+ // mirrors these as MAX_BLOCK_MS (45s) inside its 60s failClosed budget.
10
+ cursor: { budgetSeconds: 60, guardSeconds: 15 }
11
+ };
12
+ var hookMaxWaitSeconds = (agent) => Math.max(HOOK_BUDGETS[agent].budgetSeconds - HOOK_BUDGETS[agent].guardSeconds, 1);
13
+ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
14
+ const maxWait = hookMaxWaitSeconds(agent);
15
+ return Math.min(
16
+ nowMs + Math.min(Math.max(policyWaitSeconds, 0), maxWait) * 1e3,
17
+ startMs + maxWait * 1e3
18
+ );
19
+ };
20
+ var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
21
+ var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
22
+ var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
23
+ var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
24
+ var matchToolPattern = (pattern, toolName, arg) => {
25
+ const open = pattern.indexOf("(");
26
+ if (open === -1 || !pattern.endsWith(")")) {
27
+ return pattern === toolName ? "tool" : "none";
28
+ }
29
+ if (pattern.slice(0, open) !== toolName || arg === void 0) return "none";
30
+ const inner = pattern.slice(open + 1, -1);
31
+ if (inner.endsWith(":*")) {
32
+ return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
33
+ }
34
+ return arg === inner ? "exact" : "none";
35
+ };
36
+ var POLICY_ARG_KEYS = {
37
+ Bash: "command",
38
+ Edit: "file_path",
39
+ Write: "file_path"
40
+ };
41
+ var extractPolicyArg = (toolName, toolInput) => {
42
+ const key = POLICY_ARG_KEYS[toolName];
43
+ if (!key) return void 0;
44
+ const value = toolInput[key];
45
+ return typeof value === "string" ? value : void 0;
46
+ };
47
+ var SAFE_SHELL_COMMANDS = /* @__PURE__ */ new Set([
48
+ "ls",
49
+ "pwd",
50
+ "cd",
51
+ "cat",
52
+ "head",
53
+ "tail",
54
+ "wc",
55
+ "echo",
56
+ "printf",
57
+ "which",
58
+ "type",
59
+ "whoami",
60
+ "id",
61
+ "uname",
62
+ "arch",
63
+ "printenv",
64
+ "locale",
65
+ "tty",
66
+ "dirname",
67
+ "basename",
68
+ "realpath",
69
+ "readlink",
70
+ "stat",
71
+ "cut",
72
+ "nl",
73
+ "tr",
74
+ "comm",
75
+ "diff",
76
+ "cmp",
77
+ "grep",
78
+ "egrep",
79
+ "fgrep",
80
+ "jq",
81
+ "cksum",
82
+ "md5sum",
83
+ "sha1sum",
84
+ "sha256sum",
85
+ "du",
86
+ "df",
87
+ "ps",
88
+ "true"
89
+ ]);
90
+ var SAFE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
91
+ "status",
92
+ "log",
93
+ "diff",
94
+ "show",
95
+ "rev-parse",
96
+ "describe",
97
+ "blame",
98
+ "shortlog",
99
+ "ls-files",
100
+ "ls-tree",
101
+ "cat-file",
102
+ "whatchanged",
103
+ "rev-list",
104
+ "name-rev",
105
+ "for-each-ref",
106
+ "var",
107
+ "count-objects"
108
+ ]);
109
+ var GIT_WRITE_FLAGS = (token) => token === "--output" || token.startsWith("--output=");
110
+ var DANGEROUS_FIND_ACTIONS = /* @__PURE__ */ new Set([
111
+ "-exec",
112
+ "-execdir",
113
+ "-ok",
114
+ "-okdir",
115
+ "-delete",
116
+ "-fprintf",
117
+ "-fprint",
118
+ "-fprint0",
119
+ "-fls"
120
+ ]);
121
+ var SAFE_FIND_TOKENS = /* @__PURE__ */ new Set([
122
+ "-name",
123
+ "-iname",
124
+ "-path",
125
+ "-ipath",
126
+ "-wholename",
127
+ "-iwholename",
128
+ "-lname",
129
+ "-ilname",
130
+ "-regex",
131
+ "-iregex",
132
+ "-type",
133
+ "-xtype",
134
+ "-maxdepth",
135
+ "-mindepth",
136
+ "-depth",
137
+ "-mount",
138
+ "-xdev",
139
+ "-size",
140
+ "-empty",
141
+ "-perm",
142
+ "-readable",
143
+ "-writable",
144
+ "-executable",
145
+ "-mtime",
146
+ "-mmin",
147
+ "-atime",
148
+ "-amin",
149
+ "-ctime",
150
+ "-cmin",
151
+ "-newer",
152
+ "-newermt",
153
+ "-anewer",
154
+ "-cnewer",
155
+ "-user",
156
+ "-uid",
157
+ "-group",
158
+ "-gid",
159
+ "-nouser",
160
+ "-nogroup",
161
+ "-samefile",
162
+ "-inum",
163
+ "-links",
164
+ "-print",
165
+ "-print0",
166
+ "-printf",
167
+ "-ls",
168
+ "-quit",
169
+ "-prune",
170
+ "-follow",
171
+ "-noleaf",
172
+ "-ignore_readdir_race",
173
+ "-noignore_readdir_race",
174
+ "-true",
175
+ "-false",
176
+ "-not",
177
+ "-and",
178
+ "-or",
179
+ "-o",
180
+ "-a",
181
+ "-P",
182
+ "-L",
183
+ "-H",
184
+ "-D",
185
+ "-O"
186
+ ]);
187
+ var SAFE_REDIRECTION = /\s*(?:[0-9]*>&[0-9-]+|(?:[0-9]*|&)>>?\s*\/dev\/null)/g;
188
+ var basenameOf = (token) => {
189
+ const slash = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
190
+ return slash === -1 ? token : token.slice(slash + 1);
191
+ };
192
+ var TRUSTED_EXE_PREFIXES = [
193
+ "/bin/",
194
+ "/sbin/",
195
+ "/usr/bin/",
196
+ "/usr/sbin/",
197
+ "/usr/local/bin/",
198
+ "/opt/homebrew/bin/"
199
+ ];
200
+ var isTrustedExecutable = (first) => {
201
+ if (!first.includes("/")) return true;
202
+ if (first.includes("..")) return false;
203
+ return TRUSTED_EXE_PREFIXES.some((prefix) => first.startsWith(prefix));
204
+ };
205
+ var tokenizeShellCommand = (command) => {
206
+ const tokens = [];
207
+ let current = "";
208
+ let quote = null;
209
+ let started = false;
210
+ for (const ch of command) {
211
+ if (quote) {
212
+ if (ch === quote) quote = null;
213
+ else current += ch;
214
+ started = true;
215
+ continue;
216
+ }
217
+ if (ch === '"' || ch === "'") {
218
+ quote = ch;
219
+ started = true;
220
+ continue;
221
+ }
222
+ if (ch === " " || ch === " ") {
223
+ if (started) {
224
+ tokens.push(current);
225
+ current = "";
226
+ started = false;
227
+ }
228
+ continue;
229
+ }
230
+ current += ch;
231
+ started = true;
232
+ }
233
+ if (quote) return null;
234
+ if (started) tokens.push(current);
235
+ return tokens;
236
+ };
237
+ var isSafeSimpleCommand = (segment) => {
238
+ const tokens = tokenizeShellCommand(segment.trim());
239
+ if (!tokens || tokens.length === 0) return false;
240
+ const first = tokens[0];
241
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first) || first.includes("$")) return false;
242
+ if (!isTrustedExecutable(first)) return false;
243
+ const exe = basenameOf(first);
244
+ if (exe === "git") {
245
+ const sub = tokens[1];
246
+ if (!sub || sub.startsWith("-")) return false;
247
+ if (!SAFE_GIT_SUBCOMMANDS.has(sub)) return false;
248
+ return !tokens.some(GIT_WRITE_FLAGS);
249
+ }
250
+ if (exe === "find") {
251
+ for (let i = 1; i < tokens.length; i += 1) {
252
+ const t = tokens[i];
253
+ if (t === "(" || t === ")" || t === "!" || t === "\\(" || t === "\\)" || t === "\\!") continue;
254
+ if (t.startsWith("-") && (DANGEROUS_FIND_ACTIONS.has(t) || !SAFE_FIND_TOKENS.has(t))) return false;
255
+ }
256
+ return true;
257
+ }
258
+ return SAFE_SHELL_COMMANDS.has(exe);
259
+ };
260
+ var isSafeReadOnlyCommand = (command) => {
261
+ const trimmed = command.trim();
262
+ if (!trimmed || trimmed.length > 2e3) return false;
263
+ if (/[`\n\r{}]/.test(trimmed)) return false;
264
+ if (/;|\|\|/.test(trimmed)) return false;
265
+ if (/\\(?![()!])/.test(trimmed)) return false;
266
+ if (/(?<!\\)[()]/.test(trimmed)) return false;
267
+ const noRedir = trimmed.replace(SAFE_REDIRECTION, " ");
268
+ if (/[<>]/.test(noRedir)) return false;
269
+ if (noRedir.replace(/&&/g, " ").includes("&")) return false;
270
+ for (const andPart of noRedir.split("&&")) {
271
+ if (!andPart.trim()) return false;
272
+ for (const segment of andPart.split("|")) {
273
+ if (!isSafeSimpleCommand(segment)) return false;
274
+ }
275
+ }
276
+ return true;
277
+ };
278
+ var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
279
+ var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
280
+ var ACTION_BODY_MAX = 4e3;
281
+ var DECISION_LINE_MAX = 500;
282
+ var SECRET_REDACTION_RULES = [
283
+ { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
284
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
285
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
286
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
287
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
288
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
289
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
290
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
291
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
292
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
293
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
294
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
295
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
296
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
297
+ { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
298
+ {
299
+ pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
300
+ replacement: "$1[redacted]"
301
+ }
302
+ ];
303
+ var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
304
+ var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
305
+ var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
306
+
307
+ export {
308
+ isApprovalMode,
309
+ HOOK_BUDGETS,
310
+ hookWaitDeadline,
311
+ hookWaitClamped,
312
+ effectiveWaitSeconds,
313
+ matchRankWeight,
314
+ matchToolPattern,
315
+ extractPolicyArg,
316
+ isSafeReadOnlyCommand,
317
+ isValidApiKey,
318
+ ACTION_BODY_MAX,
319
+ DECISION_LINE_MAX,
320
+ redactSecrets,
321
+ redactSecretsDeep
322
+ };
@@ -80,7 +80,7 @@ declare const handlePostToolUse: (input: {
80
80
  cwd?: string;
81
81
  session_id?: string;
82
82
  transcript_path?: string;
83
- }, agent?: AgentIdentity) => Promise<void>;
83
+ }, agent?: AgentIdentity) => Promise<string | undefined>;
84
84
  interface StopHookOutput {
85
85
  readonly decision: 'block';
86
86
  readonly reason: string;
@@ -117,6 +117,12 @@ interface AskUserParams {
117
117
  interface AskUserResponse {
118
118
  correlationId: string;
119
119
  status: string;
120
+ delivery?: {
121
+ web: number;
122
+ mobile: number;
123
+ };
124
+ suppressed?: boolean;
125
+ noDevices?: boolean;
120
126
  }
121
127
  interface WaitForAnswerResponse {
122
128
  answered: boolean;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-SA4BDS7T.js";
3
+ } from "../chunk-2BHYQCF7.js";
4
4
  import "../chunk-KQYIHZ5E.js";
5
5
  import {
6
6
  askUser,
@@ -14,8 +14,9 @@ import {
14
14
  reportEvent,
15
15
  resolvePolicy,
16
16
  waitForAnswer
17
- } from "../chunk-A7DQCA2N.js";
17
+ } from "../chunk-NFKYZY74.js";
18
18
  import "../chunk-DWED7BS3.js";
19
+ import "../chunk-YRL4ZB4R.js";
19
20
  import {
20
21
  getApiKey,
21
22
  getBaseUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
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",
@@ -29,7 +29,8 @@
29
29
  "pushary-setup": "./dist/bin/pushary-setup.js",
30
30
  "pushary-clean": "./dist/bin/pushary-clean.js",
31
31
  "pushary-doctor": "./dist/bin/pushary-doctor.js",
32
- "pushary-mode": "./dist/bin/pushary-mode.js"
32
+ "pushary-mode": "./dist/bin/pushary-mode.js",
33
+ "pushary-wait": "./dist/bin/pushary-wait.js"
33
34
  },
34
35
  "files": [
35
36
  "dist",
@@ -38,7 +39,7 @@
38
39
  "scripts": {
39
40
  "build": "node scripts/bundle-plugin.mjs && tsup",
40
41
  "dev": "tsup --watch",
41
- "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts"
42
+ "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts"
42
43
  },
43
44
  "dependencies": {
44
45
  "@inquirer/prompts": "^8.4.2",