@pushary/agent-hooks 1.7.2 → 1.8.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +13 -1
  3. package/data/cursor-plugin/CHANGELOG.md +1 -0
  4. package/data/cursor-plugin/scripts/pushary-gate.mjs +2 -21
  5. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +23 -1
  6. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  7. package/data/vscode-plugin/CHANGELOG.md +1 -0
  8. package/data/vscode-plugin/scripts/pushary-gate.mjs +1 -21
  9. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  10. package/dist/bin/pushary-bell-hook.js +23 -23
  11. package/dist/bin/pushary-bell.js +9 -9
  12. package/dist/bin/pushary-claude.js +28 -28
  13. package/dist/bin/pushary-clean.js +91 -107
  14. package/dist/bin/pushary-codex-bridge.js +18 -18
  15. package/dist/bin/pushary-codex-hook.js +42 -43
  16. package/dist/bin/pushary-codex.js +13 -13
  17. package/dist/bin/pushary-connect.js +13 -13
  18. package/dist/bin/pushary-cowork.js +5 -5
  19. package/dist/bin/pushary-daemon.js +27 -27
  20. package/dist/bin/pushary-disconnect.js +18 -18
  21. package/dist/bin/pushary-doctor.js +85 -79
  22. package/dist/bin/pushary-elicitation-hook.js +25 -25
  23. package/dist/bin/pushary-gemini-bridge.js +12 -12
  24. package/dist/bin/pushary-gemini-hook.js +29 -30
  25. package/dist/bin/pushary-hook.js +32 -32
  26. package/dist/bin/pushary-login.js +12 -12
  27. package/dist/bin/pushary-logout.js +7 -7
  28. package/dist/bin/pushary-mcp.js +85 -28
  29. package/dist/bin/pushary-mode.js +9 -9
  30. package/dist/bin/pushary-notification-hook.js +28 -28
  31. package/dist/bin/pushary-opencode-hook.js +28 -29
  32. package/dist/bin/pushary-permission-denied-hook.js +32 -32
  33. package/dist/bin/pushary-permission-hook.js +31 -31
  34. package/dist/bin/pushary-post-hook.js +28 -28
  35. package/dist/bin/pushary-prompt-hook.js +28 -28
  36. package/dist/bin/pushary-session-end-hook.js +28 -28
  37. package/dist/bin/pushary-session-start-hook.js +32 -32
  38. package/dist/bin/pushary-setup.js +60 -127
  39. package/dist/bin/pushary-stats.js +5 -5
  40. package/dist/bin/pushary-status.js +15 -15
  41. package/dist/bin/pushary-stop-hook.js +28 -28
  42. package/dist/bin/pushary-stopfailure-hook.js +28 -28
  43. package/dist/bin/pushary-suggestions.js +4 -4
  44. package/dist/bin/pushary-transcript-register.js +19 -19
  45. package/dist/bin/pushary-transcripts.js +7 -7
  46. package/dist/bin/pushary-upgrade.js +231 -105
  47. package/dist/bin/pushary-wait.js +9 -9
  48. package/dist/bin/pushary.js +6 -6
  49. package/dist/{chunk-VQDJ6KB3.js → chunk-2ABTSGFT.js} +3 -3
  50. package/dist/{chunk-UFNBKYWT.js → chunk-2OY3ZZ5N.js} +3 -3
  51. package/dist/{chunk-A6WQF7FQ.js → chunk-3EVPNIBE.js} +3 -3
  52. package/dist/{chunk-L6IF4JOF.js → chunk-6J2JC2RT.js} +3 -3
  53. package/dist/{chunk-3NSRL5E7.js → chunk-7Z7Q4GGS.js} +2 -2
  54. package/dist/{chunk-NYBZJ44G.js → chunk-A2A2YTMG.js} +108 -25
  55. package/dist/{chunk-SKEW3TQX.js → chunk-AHV4LNB5.js} +1 -1
  56. package/dist/{chunk-KAJT6HPI.js → chunk-AXPCESYO.js} +3 -3
  57. package/dist/{chunk-JMSB6CJT.js → chunk-C2WCPBF7.js} +1 -1
  58. package/dist/chunk-CJVVKOSR.js +73 -0
  59. package/dist/{chunk-2B4XCMH6.js → chunk-CULUZTWQ.js} +283 -41
  60. package/dist/{chunk-6QZK4O36.js → chunk-CVQYGMSR.js} +4 -4
  61. package/dist/{chunk-SEIZQZEE.js → chunk-D2FX3EPW.js} +1 -1
  62. package/dist/{chunk-LI3C2IQY.js → chunk-DB3ONZB4.js} +2 -2
  63. package/dist/{chunk-YU54LNC6.js → chunk-DE4Y6TCC.js} +6 -6
  64. package/dist/{chunk-65RN6ZCO.js → chunk-E4FJTUH4.js} +1 -1
  65. package/dist/{chunk-RLRDPZG4.js → chunk-EPCXUP3P.js} +3 -1
  66. package/dist/{chunk-OXEF6JHD.js → chunk-EX3GAPA4.js} +1 -1
  67. package/dist/{chunk-E3PLF7YB.js → chunk-EYK3GSRH.js} +4 -4
  68. package/dist/{chunk-SJN64NJX.js → chunk-GPEGOGRJ.js} +1 -1
  69. package/dist/chunk-GU3EJCVV.js +43 -0
  70. package/dist/{chunk-YNJ2H2FD.js → chunk-HQ6G3B5R.js} +2 -2
  71. package/dist/{chunk-6NHXKTQU.js → chunk-HZVVNGEW.js} +2 -2
  72. package/dist/{chunk-ZUKZJQGR.js → chunk-IPVKH2ST.js} +1 -1
  73. package/dist/{chunk-T3PRPFDY.js → chunk-J5TIV3J6.js} +5 -5
  74. package/dist/{chunk-F6BMJOSW.js → chunk-JU6W3XAU.js} +1 -1
  75. package/dist/{chunk-AWZNDM3B.js → chunk-JYG4B33G.js} +1 -1
  76. package/dist/{chunk-ZMWX5SDA.js → chunk-JZTHGAPA.js} +8 -8
  77. package/dist/{chunk-3RO2UWJQ.js → chunk-KGDWDSNL.js} +2 -2
  78. package/dist/{chunk-MY3UZO5U.js → chunk-KVQBWI5T.js} +3 -3
  79. package/dist/{chunk-HOIGXMI7.js → chunk-LXQ5FBBD.js} +1 -1
  80. package/dist/{chunk-D4OGEWLH.js → chunk-M5ICYGHT.js} +3 -3
  81. package/dist/{chunk-YLCSKVQQ.js → chunk-M5JDZ45W.js} +4 -4
  82. package/dist/{chunk-SKMWPXGV.js → chunk-MDEPU45E.js} +183 -28
  83. package/dist/{chunk-R7ZETVNR.js → chunk-MPWXGONV.js} +3 -3
  84. package/dist/{chunk-2XOHXZRC.js → chunk-NGDD6TXY.js} +20 -5
  85. package/dist/{chunk-WWTLAZQA.js → chunk-NQTHFF4W.js} +178 -39
  86. package/dist/{chunk-KLXYMPTM.js → chunk-OKXS7WDZ.js} +1 -1
  87. package/dist/{chunk-Q7CDBJXU.js → chunk-OLQRRFBX.js} +1 -1
  88. package/dist/{chunk-VSGKQ3HI.js → chunk-P7WQEVWA.js} +2 -2
  89. package/dist/{chunk-UUTWHVSV.js → chunk-Q7E4I6UA.js} +28 -16
  90. package/dist/{chunk-WXIVUIXA.js → chunk-R7437YI3.js} +2 -2
  91. package/dist/{chunk-5GIBNUBD.js → chunk-RHR6NARM.js} +2 -2
  92. package/dist/{chunk-QSZ37ROP.js → chunk-SAXBC4AZ.js} +1 -1
  93. package/dist/{chunk-U3MS6D5X.js → chunk-SIFVFP6T.js} +1 -1
  94. package/dist/{chunk-TVJYGZXB.js → chunk-SLFABHW3.js} +40 -10
  95. package/dist/{chunk-M2JGLFFS.js → chunk-TY4JT7W3.js} +1 -1
  96. package/dist/{chunk-YU4222G2.js → chunk-UI36QSBI.js} +28 -16
  97. package/dist/{chunk-OJRB5QKN.js → chunk-VQKRG3AC.js} +6 -6
  98. package/dist/{chunk-4S7CCOXB.js → chunk-WE2J62NC.js} +31 -17
  99. package/dist/{chunk-PWNQGITG.js → chunk-ZSZSSJCV.js} +1 -1
  100. package/dist/{chunk-I7B2DNVU.js → chunk-ZVSKMPBZ.js} +1 -1
  101. package/dist/{reapply-D6RSDYCK.js → reapply-CW3MA66V.js} +9 -9
  102. package/dist/src/index.js +20 -20
  103. package/package.json +1 -1
  104. package/dist/chunk-INR3RVQ3.js +0 -92
  105. package/dist/chunk-IZVAVXHQ.js +0 -255
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0
4
+
5
+ - **An approval you give after auto mode blocks a call is no longer lost when the agent rewords the call.** Your approval covers only the exact call that was blocked. Claude Code tells the agent only that it may retry, and an agent that sent a changed command was blocked again, so the approval you gave from your phone or the Mac notch did nothing. Pushary now refuses the first changed call and tells the agent to send the blocked call again unchanged. If the agent ends its turn without sending it, Pushary tells it to send it.
6
+ - Claude Code no longer tells your phone and the Mac notch that it is waiting for you while its background tasks are still running. Claude Code reports itself idle after a minute at the prompt even when it will continue by itself once those tasks finish. A question from the agent still reaches you as before.
7
+ - A prompt longer than 500 characters, or a shell command longer than about 490, no longer stops your agent at its next phone approval. The hook sent the whole text and the server refused it, so Claude Code halted with "Pushary could not create a verifiable approval" and Codex and OpenCode denied the call. The question, intent, action, blocker and agent name are now cut to what the card can show, and a cut line ends in "…". The server now does the same for every older CLI, so this fix does not wait for you to update. (#1604)
8
+ - A line cut to fit, including a long diff, never ends on half an emoji. The server stores these lines as JSON, and Postgres refuses a half character, so the approval failed to save.
9
+ - `pushary doctor`, `pushary upgrade`, `pushary clean` and automatic updates recognise a global install under either package name, `@pushary/agent-hooks` or `pushary`. A machine keeps the name it already has, and setup on a new machine installs the name it was run from. Nothing changes for an existing install.
10
+ - `pushary doctor` flags a machine with both names installed globally, because uninstalling either one removes every Pushary command.
11
+ - `pushary upgrade` no longer installs over an automatic update that is still running. It says another upgrade is running, with its process id, and stops.
12
+
13
+ ## 1.7.3
14
+
15
+ - **Approval text never hides part of a command.** The value after a credential name is now hidden only up to the first space, quote or piece of shell syntax. 1.7.2 could still hide a command: `eval TOKEN="x; rm -rf ~"` read `eval TOKEN="[redacted]"`, `password= reboot` read `password= [redacted]`, and `rm -rf {password=x,/}` hid the `,/` that also deletes `/`. A private key block is hidden only when it holds nothing but the key. Tokens, keys and quoted values without spaces are hidden as before. A quoted value with spaces now shows what follows its first word, as in `--password='[redacted] pass'`. Logs and error reports still hide the whole value.
16
+
3
17
  ## 1.7.2
4
18
 
5
19
  - **Approval text shows the whole command again.** A credential name that closes a quoted string, as in `grep -rl "password=" . | xargs rm -f`, made 1.7.1 redact everything after it, so the approval read `grep -rl "password="[redacted]"` and hid the `rm`. The value after a credential name is still redacted, and the rest of the command is shown.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @pushary/agent-hooks
1
+ # Pushary
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@pushary/agent-hooks.svg)](https://www.npmjs.com/package/@pushary/agent-hooks)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@pushary/agent-hooks.svg)](https://www.npmjs.com/package/@pushary/agent-hooks)
@@ -100,6 +100,18 @@ Verify it worked:
100
100
  npx @pushary/agent-hooks@latest doctor
101
101
  ```
102
102
 
103
+ Setup installs this package globally with npm, at the version you ran, because the
104
+ hooks run from that install. You never install it by hand.
105
+
106
+ ## Remove it
107
+
108
+ ```bash
109
+ npx @pushary/agent-hooks@latest clean
110
+ ```
111
+
112
+ It removes the hooks, the MCP entries, the stored key and the background service.
113
+ Add `--dry-run` to see what it would remove first.
114
+
103
115
  ## Supported agents
104
116
 
105
117
  | Agent | Notify | Ask | Gate |
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## 0.2.3
4
4
 
5
+ - Approval text never hides part of a command. The value after a credential name is hidden only up to the first space, quote or piece of shell syntax, so `echo "password=" && rm -rf ~` and `eval TOKEN="x; rm -rf ~"` both show the `rm`. A private key block is hidden only when it holds nothing but the key.
5
6
  - Updates and Terminal hand an approval back to Cursor promptly. The gate no longer waits for a phone answer that Updates never asks for, and it withdraws the phone question before Cursor decides. An answer that arrives in that moment still counts.
6
7
  - A file change that goes back to the agent's own permissions is refused with the reason, because Cursor file hooks cannot show a local approval. Choose Every time for file changes you want to approve from your phone.
7
8
  - With no phone or other notification channel connected, a file change is refused with a message that says so. Shell commands and MCP calls still open Cursor's own prompt with "No device connected, approve here."
@@ -2,6 +2,8 @@
2
2
  // Pushary Cursor hooks: server policy, fenced phone approvals, and lifecycle telemetry.
3
3
  // Dependency-free for marketplace installs. Regenerate agent-hooks/data after edits.
4
4
 
5
+ import { redactSecrets, redactSecretsDeep } from './redaction.mjs'
6
+ export { redactSecrets } from './redaction.mjs'
5
7
  import { createHash, randomUUID } from 'node:crypto'
6
8
  import { hostname } from 'node:os'
7
9
  import { existsSync, readFileSync } from 'node:fs'
@@ -294,27 +296,6 @@ export const describeNetworkFailure = (error, env = process.env) => {
294
296
  // line reached the lock screen verbatim.
295
297
  const ACTION_BODY_MAX = 4000
296
298
  const ACTION_BODY_TRUNCATION_MARKER = '\n… [truncated]'
297
- const REDACTION_RULES = [
298
- [/-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, '[redacted key]'],
299
- [/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[redacted]'],
300
- [/\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, '[redacted]'],
301
- [/\bwhsec_[A-Za-z0-9]{16,}\b/g, '[redacted]'],
302
- [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, '[redacted]'],
303
- [/\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, '[redacted]'],
304
- [/\bglpat-[A-Za-z0-9_-]{20,}\b/g, '[redacted]'],
305
- [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, '[redacted]'],
306
- [/\bAIza[A-Za-z0-9_-]{35}\b/g, '[redacted]'],
307
- [/\bAKIA[0-9A-Z]{16}\b/g, '[redacted]'],
308
- [/\bnpm_[A-Za-z0-9]{36}\b/g, '[redacted]'],
309
- [/\bxai-[A-Za-z0-9]{16,}\b/g, '[redacted]'],
310
- [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '[redacted]'],
311
- [/\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, 'bearer [redacted]'],
312
- [/\bauthorization:\s*\S+/gi, 'authorization: [redacted]'],
313
- [/((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)(\"[^\"]*\"|'[^']*'|\S+)/gi, '$1[redacted]'],
314
- ]
315
- const HIGH_ENTROPY_RULE = [/[A-Za-z0-9+/]{40,}={0,2}/g, '[redacted]']
316
- export const redactSecrets = (text) => REDACTION_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), text)
317
- const redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE[0], HIGH_ENTROPY_RULE[1])
318
299
  const capActionBody = (text) =>
319
300
  text.length <= ACTION_BODY_MAX ? text : `${text.slice(0, ACTION_BODY_MAX - ACTION_BODY_TRUNCATION_MARKER.length)}${ACTION_BODY_TRUNCATION_MARKER}`
320
301
  const deriveActionBody = (command) => capActionBody(redactSecretsDeep(command))
@@ -14,7 +14,7 @@
14
14
 
15
15
  import assert from 'node:assert/strict'
16
16
  import { describe, it } from 'node:test'
17
- import { describeRequest, normalizeRepoRemote } from './pushary-gate.mjs'
17
+ import { describeRequest, normalizeRepoRemote, redactSecrets } from './pushary-gate.mjs'
18
18
 
19
19
  describe('repo identity', () => {
20
20
  it('collapses the forms of one remote', () => {
@@ -113,3 +113,25 @@ describe('what the gate was asked about', () => {
113
113
  assert.equal(describeRequest({}), null)
114
114
  })
115
115
  })
116
+
117
+ describe('approval text redaction', () => {
118
+ it('shows a command whole when a credential name only sits next to shell syntax', () => {
119
+ for (const command of [
120
+ 'echo "password=" && rm -rf ~ && echo "x"',
121
+ 'find / -name "token:" -o -delete -o -name "x"',
122
+ "grep -rl foo'password=' . | xargs rm -f 'important file'",
123
+ 'grep "authorization:";rm important-file',
124
+ 'echo "-----BEGIN PRIVATE KEY-----" && rm -rf ~ && echo "-----END PRIVATE KEY-----"',
125
+ 'password= reboot',
126
+ ]) {
127
+ assert.equal(redactSecrets(command), command)
128
+ }
129
+ })
130
+
131
+ it('stops a hidden value at the first space, quote or piece of shell syntax', () => {
132
+ assert.equal(redactSecrets('eval TOKEN="x; rm -rf ~"'), 'eval TOKEN="[redacted]; rm -rf ~"')
133
+ assert.equal(redactSecrets('TOKEN=abc;rm file'), 'TOKEN=[redacted];rm file')
134
+ assert.equal(redactSecrets('rm -rf {password=x,/}'), 'rm -rf {password=[redacted],/}')
135
+ assert.equal(redactSecrets('export API_KEY="abc123" && rm -rf build'), 'export API_KEY="[redacted]" && rm -rf build')
136
+ })
137
+ })
@@ -0,0 +1,51 @@
1
+ // Generated by scripts/generate-editor-redaction.mjs.
2
+ const CREDENTIAL_NAME = String.raw `(?:authorization|secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)["']?`;
3
+ const CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}\s*[=:]\s*`;
4
+ const DISPLAYED_CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}(?:[ \t]*:[ \t]*|[ \t]+=[ \t]*|=)`;
5
+ const SHELL_INERT_UNIT = String.raw `(?:[A-Za-z0-9_\-./+=:@%^!#]|\\[^\s'"])`;
6
+ const SHELL_INERT_SINGLE_QUOTED_UNIT = String.raw `[A-Za-z0-9_\-./+=:@%^!#\\]`;
7
+ const SHELL_INERT_URL_PASSWORD_UNIT = String.raw `(?:[A-Za-z0-9_\-.+=:%^!#]|\\[^\s'"@/])`;
8
+ const PEM_LINE_BREAK = String.raw `(?:\r?\n|(?:\\r)?\\n)`;
9
+ const credentialAssignmentRule = (key, doubleQuoted, singleQuoted, bare) => ({
10
+ pattern: new RegExp(String.raw `(${key})(?:(")${doubleQuoted}|(')${singleQuoted}|(["']?)${bare})`, 'gi'),
11
+ replacement: '$1$2$3$4[redacted]$2$3',
12
+ });
13
+ const DISPLAYED_SECRET_SHAPES = {
14
+ privateKeyBody: String.raw `${PEM_LINE_BREAK}?(?:(?:Proc-Type|DEK-Info): [A-Za-z0-9,-]+${PEM_LINE_BREAK})*(?:[A-Za-z0-9+/=\r\n]|\\[rn])*?`,
15
+ urlPassword: `${SHELL_INERT_URL_PASSWORD_UNIT}+`,
16
+ authorizationValue: `${SHELL_INERT_UNIT}+`,
17
+ credentialAssignment: credentialAssignmentRule(DISPLAYED_CREDENTIAL_KEY, `${SHELL_INERT_UNIT}*"`, `${SHELL_INERT_SINGLE_QUOTED_UNIT}*'`, `${SHELL_INERT_UNIT}+`),
18
+ };
19
+ const CUT_OFF_SECRET_SHAPES = {
20
+ privateKeyBody: String.raw `[\s\S]*?`,
21
+ urlPassword: String.raw `[^\s@/]+`,
22
+ authorizationValue: String.raw `\S+`,
23
+ credentialAssignment: credentialAssignmentRule(CREDENTIAL_KEY, String.raw `(?:\\[\s\S]|[^"\\])*(?:"|\\?$)`, "[^']*(?:'|$)", String.raw `\S+`),
24
+ };
25
+ const secretRedactionRules = (shapes) => [
26
+ { pattern: /\bpk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g, replacement: '[redacted]' },
27
+ { pattern: new RegExp(String.raw `-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----${shapes.privateKeyBody}-----END[A-Z0-9 ]*PRIVATE KEY-----`, 'g'), replacement: '[redacted key]' },
28
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: '[redacted]' },
29
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: '[redacted]' },
30
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
31
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: '[redacted]' },
32
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: '[redacted]' },
33
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: '[redacted]' },
34
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: '[redacted]' },
35
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: '[redacted]' },
36
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: '[redacted]' },
37
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[redacted]' },
38
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
39
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: '[redacted]' },
40
+ { pattern: new RegExp(String.raw `\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)${shapes.urlPassword}@`, 'gi'), replacement: '$1[redacted]@' },
41
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: 'bearer [redacted]' },
42
+ { pattern: new RegExp(String.raw `\bauthorization:\s*${shapes.authorizationValue}`, 'gi'), replacement: 'authorization: [redacted]' },
43
+ shapes.credentialAssignment,
44
+ ];
45
+ export const SECRET_REDACTION_RULES = secretRedactionRules(DISPLAYED_SECRET_SHAPES);
46
+ const CUT_OFF_TEXT_REDACTION_RULES = secretRedactionRules(CUT_OFF_SECRET_SHAPES);
47
+ const HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: '[redacted]' };
48
+ const applyRedactionRules = (rules, text) => rules.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
49
+ export const redactSecrets = (text) => applyRedactionRules(SECRET_REDACTION_RULES, text);
50
+ export const redactSecretsFromContent = (text) => applyRedactionRules(CUT_OFF_TEXT_REDACTION_RULES, text);
51
+ export const redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## 0.2.3
4
4
 
5
+ - Approval text never hides part of a command. The value after a credential name is hidden only up to the first space, quote or piece of shell syntax, so `echo "password=" && rm -rf ~` and `eval TOKEN="x; rm -rf ~"` both show the `rm`. A private key block is hidden only when it holds nothing but the key.
5
6
  - Updates hands an approval back to VS Code promptly. The gate no longer waits for a phone answer that Updates never asks for, and it withdraws the phone question before VS Code asks. An answer that arrives in that moment still counts.
6
7
  - With no phone or other notification channel connected, VS Code's prompt says "No device connected, approve here." instead of naming a delivery mode.
7
8
  - Approval questions carry the repository and the tool name the gate judged, so routing and repository rules apply to the question as they did to the gate. A file edit is asked about with its full path, so path rules and routing match it. A path longer than 80 characters falls back to the file type instead of making the question fail.
@@ -2,6 +2,7 @@
2
2
  // Pushary VS Code hooks: server policy, fenced phone approvals, and lifecycle telemetry.
3
3
  // Dependency-free for marketplace installs. Regenerate agent-hooks/data after edits.
4
4
 
5
+ import { redactSecrets, redactSecretsDeep } from './redaction.mjs'
5
6
  import { createHash, randomUUID } from 'node:crypto'
6
7
  import { homedir, hostname } from 'node:os'
7
8
  import { existsSync, readFileSync } from 'node:fs'
@@ -337,27 +338,6 @@ const ACTION_BODY_TRUNCATION_MARKER = '\n… [truncated]'
337
338
  // One combined list used to serve both, which meant the only text this gate
338
339
  // scrubbed was the action body. The question and the notification body carried
339
340
  // the raw command.
340
- const REDACTION_RULES = [
341
- [/-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, '[redacted key]'],
342
- [/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[redacted]'],
343
- [/\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, '[redacted]'],
344
- [/\bwhsec_[A-Za-z0-9]{16,}\b/g, '[redacted]'],
345
- [/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, '[redacted]'],
346
- [/\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, '[redacted]'],
347
- [/\bglpat-[A-Za-z0-9_-]{20,}\b/g, '[redacted]'],
348
- [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, '[redacted]'],
349
- [/\bAIza[A-Za-z0-9_-]{35}\b/g, '[redacted]'],
350
- [/\bAKIA[0-9A-Z]{16}\b/g, '[redacted]'],
351
- [/\bnpm_[A-Za-z0-9]{36}\b/g, '[redacted]'],
352
- [/\bxai-[A-Za-z0-9]{16,}\b/g, '[redacted]'],
353
- [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '[redacted]'],
354
- [/\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, 'bearer [redacted]'],
355
- [/\bauthorization:\s*\S+/gi, 'authorization: [redacted]'],
356
- [/((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]'],
357
- ]
358
- const HIGH_ENTROPY_RULE = [/[A-Za-z0-9+/]{40,}={0,2}/g, '[redacted]']
359
- const redactSecrets = (text) => REDACTION_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), text)
360
- const redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE[0], HIGH_ENTROPY_RULE[1])
361
341
  const capActionBody = (text) =>
362
342
  text.length <= ACTION_BODY_MAX ? text : `${text.slice(0, ACTION_BODY_MAX - ACTION_BODY_TRUNCATION_MARKER.length)}${ACTION_BODY_TRUNCATION_MARKER}`
363
343
  const deriveActionBody = (command) => capActionBody(redactSecretsDeep(command))
@@ -0,0 +1,51 @@
1
+ // Generated by scripts/generate-editor-redaction.mjs.
2
+ const CREDENTIAL_NAME = String.raw `(?:authorization|secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)["']?`;
3
+ const CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}\s*[=:]\s*`;
4
+ const DISPLAYED_CREDENTIAL_KEY = String.raw `${CREDENTIAL_NAME}(?:[ \t]*:[ \t]*|[ \t]+=[ \t]*|=)`;
5
+ const SHELL_INERT_UNIT = String.raw `(?:[A-Za-z0-9_\-./+=:@%^!#]|\\[^\s'"])`;
6
+ const SHELL_INERT_SINGLE_QUOTED_UNIT = String.raw `[A-Za-z0-9_\-./+=:@%^!#\\]`;
7
+ const SHELL_INERT_URL_PASSWORD_UNIT = String.raw `(?:[A-Za-z0-9_\-.+=:%^!#]|\\[^\s'"@/])`;
8
+ const PEM_LINE_BREAK = String.raw `(?:\r?\n|(?:\\r)?\\n)`;
9
+ const credentialAssignmentRule = (key, doubleQuoted, singleQuoted, bare) => ({
10
+ pattern: new RegExp(String.raw `(${key})(?:(")${doubleQuoted}|(')${singleQuoted}|(["']?)${bare})`, 'gi'),
11
+ replacement: '$1$2$3$4[redacted]$2$3',
12
+ });
13
+ const DISPLAYED_SECRET_SHAPES = {
14
+ privateKeyBody: String.raw `${PEM_LINE_BREAK}?(?:(?:Proc-Type|DEK-Info): [A-Za-z0-9,-]+${PEM_LINE_BREAK})*(?:[A-Za-z0-9+/=\r\n]|\\[rn])*?`,
15
+ urlPassword: `${SHELL_INERT_URL_PASSWORD_UNIT}+`,
16
+ authorizationValue: `${SHELL_INERT_UNIT}+`,
17
+ credentialAssignment: credentialAssignmentRule(DISPLAYED_CREDENTIAL_KEY, `${SHELL_INERT_UNIT}*"`, `${SHELL_INERT_SINGLE_QUOTED_UNIT}*'`, `${SHELL_INERT_UNIT}+`),
18
+ };
19
+ const CUT_OFF_SECRET_SHAPES = {
20
+ privateKeyBody: String.raw `[\s\S]*?`,
21
+ urlPassword: String.raw `[^\s@/]+`,
22
+ authorizationValue: String.raw `\S+`,
23
+ credentialAssignment: credentialAssignmentRule(CREDENTIAL_KEY, String.raw `(?:\\[\s\S]|[^"\\])*(?:"|\\?$)`, "[^']*(?:'|$)", String.raw `\S+`),
24
+ };
25
+ const secretRedactionRules = (shapes) => [
26
+ { pattern: /\bpk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g, replacement: '[redacted]' },
27
+ { pattern: new RegExp(String.raw `-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----${shapes.privateKeyBody}-----END[A-Z0-9 ]*PRIVATE KEY-----`, 'g'), replacement: '[redacted key]' },
28
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: '[redacted]' },
29
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: '[redacted]' },
30
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
31
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: '[redacted]' },
32
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: '[redacted]' },
33
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: '[redacted]' },
34
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: '[redacted]' },
35
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: '[redacted]' },
36
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: '[redacted]' },
37
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[redacted]' },
38
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: '[redacted]' },
39
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: '[redacted]' },
40
+ { pattern: new RegExp(String.raw `\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)${shapes.urlPassword}@`, 'gi'), replacement: '$1[redacted]@' },
41
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: 'bearer [redacted]' },
42
+ { pattern: new RegExp(String.raw `\bauthorization:\s*${shapes.authorizationValue}`, 'gi'), replacement: 'authorization: [redacted]' },
43
+ shapes.credentialAssignment,
44
+ ];
45
+ export const SECRET_REDACTION_RULES = secretRedactionRules(DISPLAYED_SECRET_SHAPES);
46
+ const CUT_OFF_TEXT_REDACTION_RULES = secretRedactionRules(CUT_OFF_SECRET_SHAPES);
47
+ const HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: '[redacted]' };
48
+ const applyRedactionRules = (rules, text) => rules.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
49
+ export const redactSecrets = (text) => applyRedactionRules(SECRET_REDACTION_RULES, text);
50
+ export const redactSecretsFromContent = (text) => applyRedactionRules(CUT_OFF_TEXT_REDACTION_RULES, text);
51
+ export const redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
@@ -1,38 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-ZUKZJQGR.js";
4
+ } from "../chunk-IPVKH2ST.js";
5
5
  import {
6
6
  readHookInput
7
- } from "../chunk-3RO2UWJQ.js";
8
- import "../chunk-ZMWX5SDA.js";
7
+ } from "../chunk-KGDWDSNL.js";
8
+ import "../chunk-JZTHGAPA.js";
9
9
  import "../chunk-SGZJ5LYH.js";
10
- import "../chunk-E3PLF7YB.js";
11
- import "../chunk-I7B2DNVU.js";
12
- import "../chunk-JMSB6CJT.js";
13
- import "../chunk-L6IF4JOF.js";
14
- import "../chunk-4S7CCOXB.js";
15
- import "../chunk-A6WQF7FQ.js";
16
- import "../chunk-KLXYMPTM.js";
17
- import "../chunk-R7ZETVNR.js";
18
- import "../chunk-QSZ37ROP.js";
19
- import "../chunk-Q7CDBJXU.js";
10
+ import "../chunk-EYK3GSRH.js";
11
+ import "../chunk-ZVSKMPBZ.js";
12
+ import "../chunk-C2WCPBF7.js";
13
+ import "../chunk-6J2JC2RT.js";
14
+ import "../chunk-WE2J62NC.js";
15
+ import "../chunk-3EVPNIBE.js";
16
+ import "../chunk-OKXS7WDZ.js";
17
+ import "../chunk-MPWXGONV.js";
18
+ import "../chunk-SAXBC4AZ.js";
19
+ import "../chunk-OLQRRFBX.js";
20
20
  import "../chunk-XHKBHWLX.js";
21
- import "../chunk-RLRDPZG4.js";
22
- import "../chunk-6QZK4O36.js";
21
+ import "../chunk-EPCXUP3P.js";
22
+ import "../chunk-CVQYGMSR.js";
23
23
  import "../chunk-POFBE6ZY.js";
24
24
  import "../chunk-GMXKITVA.js";
25
- import "../chunk-VSGKQ3HI.js";
26
- import "../chunk-UFNBKYWT.js";
27
- import "../chunk-HOIGXMI7.js";
25
+ import "../chunk-P7WQEVWA.js";
26
+ import "../chunk-2OY3ZZ5N.js";
27
+ import "../chunk-LXQ5FBBD.js";
28
28
  import "../chunk-7QLSKOSU.js";
29
29
  import "../chunk-KZERVKTD.js";
30
- import "../chunk-LI3C2IQY.js";
31
- import "../chunk-NYBZJ44G.js";
30
+ import "../chunk-DB3ONZB4.js";
31
+ import "../chunk-A2A2YTMG.js";
32
32
  import "../chunk-T7E75EKA.js";
33
- import "../chunk-SJN64NJX.js";
34
- import "../chunk-65RN6ZCO.js";
35
- import "../chunk-WWTLAZQA.js";
33
+ import "../chunk-GPEGOGRJ.js";
34
+ import "../chunk-E4FJTUH4.js";
35
+ import "../chunk-NQTHFF4W.js";
36
36
 
37
37
  // bin/pushary-bell-hook.ts
38
38
  var main = async () => {
@@ -5,37 +5,37 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-ZUKZJQGR.js";
8
+ } from "../chunk-IPVKH2ST.js";
9
9
  import {
10
10
  addBellHooks,
11
11
  bellInstalled,
12
12
  removeBellHooks
13
- } from "../chunk-KLXYMPTM.js";
13
+ } from "../chunk-OKXS7WDZ.js";
14
14
  import {
15
15
  createIo
16
16
  } from "../chunk-U4TMAANR.js";
17
17
  import {
18
18
  exitOnHelpFlag
19
- } from "../chunk-6NHXKTQU.js";
19
+ } from "../chunk-HZVVNGEW.js";
20
20
  import {
21
21
  parseFlags
22
22
  } from "../chunk-GMXKITVA.js";
23
- import "../chunk-VSGKQ3HI.js";
23
+ import "../chunk-P7WQEVWA.js";
24
24
  import "../chunk-7QLSKOSU.js";
25
25
  import {
26
26
  EXIT
27
27
  } from "../chunk-KZERVKTD.js";
28
- import "../chunk-LI3C2IQY.js";
28
+ import "../chunk-DB3ONZB4.js";
29
29
  import {
30
30
  claudeSettings
31
- } from "../chunk-NYBZJ44G.js";
31
+ } from "../chunk-A2A2YTMG.js";
32
32
  import {
33
33
  readJsonSafe,
34
34
  writeJsonAtomic
35
35
  } from "../chunk-T7E75EKA.js";
36
- import "../chunk-SJN64NJX.js";
37
- import "../chunk-65RN6ZCO.js";
38
- import "../chunk-WWTLAZQA.js";
36
+ import "../chunk-GPEGOGRJ.js";
37
+ import "../chunk-E4FJTUH4.js";
38
+ import "../chunk-NQTHFF4W.js";
39
39
 
40
40
  // bin/pushary-bell.ts
41
41
  exitOnHelpFlag("bell");
@@ -8,7 +8,7 @@ import {
8
8
  describeIdleWindow,
9
9
  startProcessOwnership,
10
10
  withSpawnProcessOwnership
11
- } from "../chunk-PWNQGITG.js";
11
+ } from "../chunk-ZSZSSJCV.js";
12
12
  import {
13
13
  announceSubscriptionRequired,
14
14
  isDeferAnswer
@@ -18,24 +18,24 @@ import {
18
18
  loadSessionKey,
19
19
  registerTranscript,
20
20
  registeredTranscriptEnabled
21
- } from "../chunk-ZMWX5SDA.js";
21
+ } from "../chunk-JZTHGAPA.js";
22
22
  import "../chunk-SGZJ5LYH.js";
23
- import "../chunk-E3PLF7YB.js";
24
- import "../chunk-I7B2DNVU.js";
25
- import "../chunk-JMSB6CJT.js";
26
- import "../chunk-L6IF4JOF.js";
23
+ import "../chunk-EYK3GSRH.js";
24
+ import "../chunk-ZVSKMPBZ.js";
25
+ import "../chunk-C2WCPBF7.js";
26
+ import "../chunk-6J2JC2RT.js";
27
27
  import {
28
28
  ensureDaemonRunning
29
- } from "../chunk-4S7CCOXB.js";
30
- import "../chunk-A6WQF7FQ.js";
31
- import "../chunk-KLXYMPTM.js";
29
+ } from "../chunk-WE2J62NC.js";
30
+ import "../chunk-3EVPNIBE.js";
31
+ import "../chunk-OKXS7WDZ.js";
32
32
  import {
33
33
  encryptRecord
34
- } from "../chunk-R7ZETVNR.js";
35
- import "../chunk-QSZ37ROP.js";
34
+ } from "../chunk-MPWXGONV.js";
35
+ import "../chunk-SAXBC4AZ.js";
36
36
  import {
37
37
  transcriptsEnabled
38
- } from "../chunk-Q7CDBJXU.js";
38
+ } from "../chunk-OLQRRFBX.js";
39
39
  import {
40
40
  needsShell,
41
41
  spawnClaude
@@ -50,7 +50,7 @@ import {
50
50
  repoKeyFor,
51
51
  reportEvent,
52
52
  scopePathFor
53
- } from "../chunk-2B4XCMH6.js";
53
+ } from "../chunk-CULUZTWQ.js";
54
54
  import {
55
55
  DEFAULT_SESSION,
56
56
  markPendingCommandDelivered,
@@ -60,7 +60,7 @@ import {
60
60
  removePendingCommand,
61
61
  savePendingCommand
62
62
  } from "../chunk-N6IOSZOE.js";
63
- import "../chunk-RLRDPZG4.js";
63
+ import "../chunk-EPCXUP3P.js";
64
64
  import {
65
65
  DECISION_STOP_REASON,
66
66
  UNVERIFIABLE_APPROVAL_REASON,
@@ -71,36 +71,36 @@ import {
71
71
  nextLongPollMs,
72
72
  waitForAnswer,
73
73
  withdrawQuestion
74
- } from "../chunk-2XOHXZRC.js";
75
- import "../chunk-6QZK4O36.js";
74
+ } from "../chunk-NGDD6TXY.js";
75
+ import "../chunk-CVQYGMSR.js";
76
76
  import "../chunk-POFBE6ZY.js";
77
77
  import "../chunk-GMXKITVA.js";
78
- import "../chunk-VSGKQ3HI.js";
78
+ import "../chunk-P7WQEVWA.js";
79
79
  import {
80
80
  SubscriptionRequiredError
81
- } from "../chunk-D4OGEWLH.js";
82
- import "../chunk-VQDJ6KB3.js";
83
- import "../chunk-UFNBKYWT.js";
81
+ } from "../chunk-M5ICYGHT.js";
82
+ import "../chunk-2ABTSGFT.js";
83
+ import "../chunk-2OY3ZZ5N.js";
84
84
  import {
85
85
  getMachineId
86
- } from "../chunk-HOIGXMI7.js";
86
+ } from "../chunk-LXQ5FBBD.js";
87
87
  import "../chunk-7QLSKOSU.js";
88
88
  import "../chunk-KZERVKTD.js";
89
- import "../chunk-LI3C2IQY.js";
90
- import "../chunk-NYBZJ44G.js";
89
+ import "../chunk-DB3ONZB4.js";
90
+ import "../chunk-A2A2YTMG.js";
91
91
  import "../chunk-T7E75EKA.js";
92
92
  import {
93
93
  getApiKey,
94
94
  getBaseUrl
95
- } from "../chunk-SJN64NJX.js";
95
+ } from "../chunk-GPEGOGRJ.js";
96
96
  import {
97
97
  NPX_LATEST_COMMAND
98
- } from "../chunk-65RN6ZCO.js";
98
+ } from "../chunk-E4FJTUH4.js";
99
99
  import {
100
100
  KILL_REASON,
101
- redactSecrets,
101
+ redactSecretsFromContent,
102
102
  resolveGate
103
- } from "../chunk-WWTLAZQA.js";
103
+ } from "../chunk-NQTHFF4W.js";
104
104
 
105
105
  // src/wrapper/localPassthrough.ts
106
106
  var SIGNAL_NUMBERS = {
@@ -161,7 +161,7 @@ var redactDraft = (text, complete = false) => {
161
161
  const close = text.indexOf(match[1], match.index + match[0].length);
162
162
  if (close < 0 || close >= end) end = Math.min(end, match.index);
163
163
  }
164
- return redactSecrets(text.slice(0, end));
164
+ return redactSecretsFromContent(text.slice(0, end));
165
165
  };
166
166
  var DraftText = class {
167
167
  messageId = "";