@hasna/hooks 0.11.7 → 0.12.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.
- package/README.md +128 -3
- package/bin/hooks-mcp.js +39 -7
- package/bin/index.js +736 -127
- package/bin/native-safety-entry.js +15 -1718
- package/bin/serve.js +36 -9
- package/dist/index.js +49 -16
- package/dist/lib/native-safety-registration.d.ts +49 -6
- package/dist/lib/registry.d.ts +15 -0
- package/dist/native-safety.d.ts +4 -0
- package/dist/native-safety.js +8 -4
- package/dist/sdk/index.js +1 -1
- package/hooks/hook-signed-link-guard/README.md +228 -0
- package/hooks/hook-signed-link-guard/package.json +12 -0
- package/hooks/hook-signed-link-guard/src/classify.ts +1280 -0
- package/hooks/hook-signed-link-guard/src/hook.ts +72 -0
- package/hooks/hook-signed-link-guard/src/jq.ts +545 -0
- package/hooks/hook-signed-link-guard/src/links.ts +136 -0
- package/hooks/hook-signed-link-output/README.md +47 -0
- package/hooks/hook-signed-link-output/package.json +12 -0
- package/hooks/hook-signed-link-output/src/hook.ts +111 -0
- package/hooks/hook-trash-guard/src/hook.ts +204 -80
- package/hooks/native-safety-entry.ts +28 -7
- package/package.json +2 -2
- package/scripts/validate-package.ts +12 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# signed-link-guard
|
|
2
|
+
|
|
3
|
+
PreToolUse guard for shell commands, installed as `hooks run signed-link-guard`
|
|
4
|
+
with the matcher `^(Bash|Monitor)$`. It judges Claude Code's `Bash` tool and its
|
|
5
|
+
`Monitor` tool (Monitor runs a shell command and streams each stdout line to the
|
|
6
|
+
model), and Codex's `Bash`. The bundled native safety entry
|
|
7
|
+
(`hooks safety install trash-guard`) also evaluates it for every Bash and
|
|
8
|
+
Monitor command, whatever capability name the registration uses.
|
|
9
|
+
|
|
10
|
+
Composite GitHub content (bodies, comments, reviews, commit messages, check
|
|
11
|
+
links and check details) carries third-party signed action links: URLs with a
|
|
12
|
+
signature parameter, a multi-week expiry and no revoke path. A tool result stays
|
|
13
|
+
in the agent's transcript, so a printed link is an exposed capability. This
|
|
14
|
+
guard refuses a command before it runs when any `gh` invocation in it would
|
|
15
|
+
print that content. The refusal reason is always exactly:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
composite output withheld: signed-link shape; use bounded scalar projections per class disposition 799310
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it refuses
|
|
22
|
+
|
|
23
|
+
1. `gh pr view`, `gh issue view`, `gh release view`, `gh discussion view`:
|
|
24
|
+
- without `--json` (the human view prints the body), so a plain
|
|
25
|
+
`gh pr view <n>` is refused;
|
|
26
|
+
- with `--comments` / `-c`;
|
|
27
|
+
- with a `--json` field outside the scalar list below, unless a `--jq`
|
|
28
|
+
projection provably prints only bounded scalars.
|
|
29
|
+
`--web` / `-w` is allowed: it opens a browser and prints nothing. A flag's
|
|
30
|
+
last occurrence wins and `--web=false`, `-w=false` or `--web=0` is not set,
|
|
31
|
+
as gh's own flag parser reads them.
|
|
32
|
+
2. `gh pr checks` without `--json` (the table prints check links, and so does
|
|
33
|
+
`--watch`), or with a `--json` field other than `bucket, completedAt, event,
|
|
34
|
+
name, startedAt, state, workflow` (`link` and `description` are refused)
|
|
35
|
+
unless a scalar `--jq` projection selects from them.
|
|
36
|
+
3. `gh api` on REST endpoints that return bodies, comments, reviews, commit
|
|
37
|
+
messages, check runs or suites, statuses, events, deployment payloads or
|
|
38
|
+
issue and pull request objects:
|
|
39
|
+
- `repos/<o>/<r>/{pulls,issues,commits,check-runs,check-suites,statuses,
|
|
40
|
+
comments,releases,compare,events,discussions,deployments,milestones}`
|
|
41
|
+
(issue labels excepted);
|
|
42
|
+
- a single branch `repos/<o>/<r>/branches/<b>` (it carries the head
|
|
43
|
+
commit's message; the branch list, protection and rename endpoints do
|
|
44
|
+
not);
|
|
45
|
+
- `…/git/{commits,tags}`, `…/actions/{runs,jobs}` (logs excepted, see
|
|
46
|
+
below), `…/actions/workflows/<id>/runs`, webhook deliveries;
|
|
47
|
+
- `issues`, `user/issues`, `orgs/<o>/issues`, `search/{issues,commits}`,
|
|
48
|
+
user, org and network events, project items and cards, team
|
|
49
|
+
discussions.
|
|
50
|
+
|
|
51
|
+
The endpoint is normalised as the request reaches GitHub first: an
|
|
52
|
+
`https://api.github.com/` or GHES `/api/v3/` origin, the query string and
|
|
53
|
+
fragment are stripped, `%2F` and other escapes are decoded, `.` and `..`
|
|
54
|
+
are resolved and letters are lower-cased. Any other absolute URL (a
|
|
55
|
+
github.com page, a raw `.patch`) cannot be classified and is refused.
|
|
56
|
+
4. `gh api graphql` queries selecting `body`, `bodyText`, `bodyHTML`,
|
|
57
|
+
`comments`, `reviews`, `reviewThreads`, commit `message*`,
|
|
58
|
+
`autoMergeRequest { commitBody commitHeadline }`, `summary`, `text`,
|
|
59
|
+
`description`, `annotations`, `payload`, `url` or any `*Url` / `*HTML`
|
|
60
|
+
field, and a check run's or status context's `title`. The query is read
|
|
61
|
+
with a GraphQL tokenizer, so strings, block strings and `#` comments are
|
|
62
|
+
not selections; an unbalanced query counts as sensitive.
|
|
63
|
+
5. `gh api` writes whose response echoes the object: `PATCH` of an issue,
|
|
64
|
+
pull request, review or comment, and other writes on the families above.
|
|
65
|
+
Writes whose documented response is empty or scalar pass (GitHub REST
|
|
66
|
+
OpenAPI description 1.1.4): deleting a comment, reaction, label, release,
|
|
67
|
+
asset, run, deployment or milestone; re-running, cancelling or approving a
|
|
68
|
+
run or job; `PUT pulls/<n>/merge` (it returns `sha`, `merged` and a GitHub
|
|
69
|
+
status message) and `update-branch`; setting, adding or removing issue
|
|
70
|
+
labels; locking an issue. For the others use `--silent` or a scalar `--jq`.
|
|
71
|
+
6. `gh api` reads of a sensitive family without `--silent` or a `--jq` / `-q`
|
|
72
|
+
projection the guard can prove prints only bounded scalars (see below).
|
|
73
|
+
7. Other composite reads: `gh status` (it prints comment and mention
|
|
74
|
+
excerpts), `gh pr diff --patch` (each commit's full message), `gh project
|
|
75
|
+
item-list --format json` without a scalar `--jq`, and `--json` lists with a
|
|
76
|
+
composite field on `gh pr list`, `gh issue list`, `gh pr status`,
|
|
77
|
+
`gh issue status`, `gh search prs|issues|commits`, `gh release list` and
|
|
78
|
+
`gh discussion list`.
|
|
79
|
+
8. Traffic logging. `GH_DEBUG` (any value except empty, `0`, `false` and `no`;
|
|
80
|
+
gh's legacy `DEBUG` for `1`, `true`, `yes` and `api`) and `gh api
|
|
81
|
+
--verbose` make gh log the raw HTTP responses whatever `--jq` or `--silent`
|
|
82
|
+
print. Under them a call passes only when it provably fetches no link field:
|
|
83
|
+
a view, list, status or search whose `--json` fields are all scalar, or a
|
|
84
|
+
`gh api` call on an endpoint outside the families above (logs endpoints
|
|
85
|
+
excepted). Every other gh command is refused under `GH_DEBUG`, writes and
|
|
86
|
+
`gh run` included; `gh pr checks` is always refused under it, because its
|
|
87
|
+
query fetches every check's `detailsUrl` whatever `--json` selects. The
|
|
88
|
+
variable is read from assignments in the command (`GH_DEBUG=api gh …`,
|
|
89
|
+
`export GH_DEBUG=1; gh …`, `env GH_DEBUG=1 gh …`).
|
|
90
|
+
9. Fail closed: an endpoint, field list or query built from an expansion
|
|
91
|
+
(`$VAR`, `$( )`, a glob or brace list), a GraphQL query read from a file or
|
|
92
|
+
stdin, an unknown `gh` command (it may be an alias for a read above), an
|
|
93
|
+
unknown subcommand of `pr`, `issue`, `release`, `discussion` or `search`, a
|
|
94
|
+
read whose arguments `xargs` appends from stdin (`… | xargs gh pr view`),
|
|
95
|
+
and a command that cannot be parsed to its end while it mentions `gh`.
|
|
96
|
+
|
|
97
|
+
## Scalar `--json` fields
|
|
98
|
+
|
|
99
|
+
- Pull requests: `additions, assignees, author, baseRefName, baseRefOid,
|
|
100
|
+
changedFiles, closed, closedAt, createdAt, deletions, files, fullDatabaseId,
|
|
101
|
+
headRefName, headRefOid, headRepository, headRepositoryOwner, id,
|
|
102
|
+
isCrossRepository, isDraft, labels, maintainerCanModify, mergeCommit,
|
|
103
|
+
mergeStateStatus, mergeable, mergedAt, mergedBy, number,
|
|
104
|
+
potentialMergeCommit, reactionGroups, reviewDecision, reviewRequests, state,
|
|
105
|
+
title, updatedAt, url`.
|
|
106
|
+
- Issues: `assignees, author, closed, closedAt, createdAt, id, isPinned,
|
|
107
|
+
labels, number, reactionGroups, state, stateReason, subIssuesSummary, title,
|
|
108
|
+
updatedAt, url`.
|
|
109
|
+
- Releases: `apiUrl, author, createdAt, databaseId, id, isDraft, isImmutable,
|
|
110
|
+
isLatest, isPrerelease, name, publishedAt, tagName, tarballUrl,
|
|
111
|
+
targetCommitish, uploadUrl, url, zipballUrl`.
|
|
112
|
+
|
|
113
|
+
The object's own `url` is allowed: GitHub generates it from the owner,
|
|
114
|
+
repository and number, and it has no query string that could carry a
|
|
115
|
+
signature. Labels are allowed because a label description is a repository
|
|
116
|
+
setting capped at 100 characters. Refused composite fields include `body,
|
|
117
|
+
comments, reviews, latestReviews, statusCheckRollup, commits,
|
|
118
|
+
autoMergeRequest, closingIssuesReferences, closedByPullRequestsReferences,
|
|
119
|
+
blockedBy, blocking, parent, subIssues, milestone, projectCards,
|
|
120
|
+
projectItems, issueType, category, assets`, and any field a future gh adds.
|
|
121
|
+
|
|
122
|
+
## `--jq` projections
|
|
123
|
+
|
|
124
|
+
A `--jq` / `-q` filter admits a command when every output provably ends at a
|
|
125
|
+
scalar, non-free-text field. This applies to `gh api`, to `gh project
|
|
126
|
+
item-list --format json`, and to the `--json` commands above, where it admits
|
|
127
|
+
a composite field: `gh pr view 12 --json body --jq '.body | length'` passes.
|
|
128
|
+
Accepted examples: `.head.sha`, `.[] | .name`, `[.number, .title]`,
|
|
129
|
+
`{number, state}`, `"\(.number) \(.title)"`, `.check_runs[] | [.name,
|
|
130
|
+
.conclusion] | @tsv`, `.body | length`, `map(.name) | join(",")`,
|
|
131
|
+
`.[] | select(.user.login == "x") | .id`, and on a single pull request or
|
|
132
|
+
issue the counts `.commits`, `.comments` and `.review_comments`.
|
|
133
|
+
|
|
134
|
+
Scalar leaves are identifiers, numbers, counts, booleans, enums, refs, object
|
|
135
|
+
ids, timestamps and short names (`id`, `number`, `state`, `conclusion`,
|
|
136
|
+
`name`, `login`, `sha`, `title`, `created_at`, `digest`, `size_in_bytes`,
|
|
137
|
+
`run_started_at`, `date`, `wait_timer`, `ahead_by`, `behind_by`,
|
|
138
|
+
`total_commits`, …) and `html_url`. `html_url` is admitted because in every
|
|
139
|
+
response the guard refuses it is a GitHub-generated `github.com/<owner>/<repo>/…`
|
|
140
|
+
page address with at most an anchor or a GitHub filter query; the schemas where
|
|
141
|
+
someone else sets it (license, Pages site, dependency-snapshot job) are not
|
|
142
|
+
among them. Other URL fields (`url`, `details_url`, `target_url`) stay refused.
|
|
143
|
+
|
|
144
|
+
Refused: anything outside the supported jq subset (`..`, `if`, `reduce`,
|
|
145
|
+
variables, `$ENV`, `input`, dynamic object keys, regular-expression functions
|
|
146
|
+
with a non-literal pattern, `#` comments, which gojq continues across a
|
|
147
|
+
backslash-newline), any whole object (`.`, `.head`, `.[]`), any free-text leaf
|
|
148
|
+
(`.body`, `.output.summary`), anything projected out of a free-form container
|
|
149
|
+
(`payload`, `inputs`, check `output`, `config`, …), and anything projected out
|
|
150
|
+
of an object or array the filter built from a composite field
|
|
151
|
+
(`{name: .body} | .name`, `[.body] | .[0]`). `--verbose` and `--template` void
|
|
152
|
+
a projection.
|
|
153
|
+
|
|
154
|
+
## Where commands can hide
|
|
155
|
+
|
|
156
|
+
Command position is honoured (only a `gh` word in command position counts,
|
|
157
|
+
after assignments, keywords and `env`, `sudo`, `timeout`, `nice`, `command`,
|
|
158
|
+
`exec`, `nohup`, `time` and similar wrappers), and the guard follows:
|
|
159
|
+
|
|
160
|
+
- pipelines, `&&`, `||`, `;`, subshells, `{ …; }` groups and `function`
|
|
161
|
+
bodies;
|
|
162
|
+
- `$( … )` and backticks, also inside double quotes and unquoted
|
|
163
|
+
here-documents; a here-document body inside `$( )` is data, so the default
|
|
164
|
+
write spelling `gh pr comment 1 --body "$(cat <<'EOF' … EOF)"` passes
|
|
165
|
+
whatever the body says (apostrophes, an unbalanced `)`, nested quotes, or
|
|
166
|
+
the text of a refused command);
|
|
167
|
+
- `bash|sh|zsh -c`, also when the string is built by `$(cat <<EOF …)` or
|
|
168
|
+
`$(echo …)`, `eval`, `env -S`, `su|runuser|script|flock -c`, `ssh <host>
|
|
169
|
+
<command>`, `gh codespace ssh … -- <command>` and `watch`;
|
|
170
|
+
- text a shell reads as its script: literal `echo`, `printf` or `cat <<EOF`
|
|
171
|
+
output piped into `bash` or `sh`, here-documents and here-strings fed to a
|
|
172
|
+
shell, `source <(…)`, `. <(…)` and `. /dev/stdin`;
|
|
173
|
+
- `xargs … gh …` and `find … -exec gh … ;`;
|
|
174
|
+
- runners that pass a bare `gh` word on (`secrets exec … -- gh`, `op run --
|
|
175
|
+
gh`, `unbuffer gh`, `npx`, `bun x`, `docker`, …).
|
|
176
|
+
|
|
177
|
+
A command word built by expansion is judged by its literal basename when it
|
|
178
|
+
has one: `"$HOME/.bun/bin/tool" status` is not gh, `"$HOME/bin/gh" pr view 1`
|
|
179
|
+
is. A word with no readable name (`$GH`, `"$(command -v gh)"`) is judged as gh.
|
|
180
|
+
Any other program's `gh` argument is data: `node x.js gh pr view 1` is not a
|
|
181
|
+
gh call.
|
|
182
|
+
|
|
183
|
+
## Always allowed
|
|
184
|
+
|
|
185
|
+
`gh pr diff` (without `--patch`), `gh pr list` / `gh issue list` tables,
|
|
186
|
+
`gh pr view --json` with scalar fields, every `gh` write subcommand (`create`,
|
|
187
|
+
`comment`, `edit`, `merge`, `review`, …; `--body` and `--body-file` are input,
|
|
188
|
+
not output), `gh run`, `gh repo`, `gh workflow`, `gh release download`, `gh
|
|
189
|
+
api` on other endpoints, `--help`, and every command that does not run `gh`.
|
|
190
|
+
|
|
191
|
+
Workflow logs are allowed: `gh run view --log`, `--log-failed` and `gh api
|
|
192
|
+
…/actions/runs/<id>/logs` or `…/actions/jobs/<id>/logs` print the
|
|
193
|
+
repository's own step output, not the composite objects of this class, and
|
|
194
|
+
the two routes are treated the same. They are downloads through a redirect to
|
|
195
|
+
a signed storage URL that gh follows without printing; `--verbose` and
|
|
196
|
+
`GH_DEBUG` print that redirect, so under them the logs endpoints are refused.
|
|
197
|
+
|
|
198
|
+
## Scalar alternatives
|
|
199
|
+
|
|
200
|
+
| Instead of | Use |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `gh pr view 12` | `gh pr view 12 --json number,title,state,headRefOid,url` |
|
|
203
|
+
| reading a body | `gh pr view 12 --json body --jq '.body \| length'`, or `gh pr view 12 --web` |
|
|
204
|
+
| `gh pr checks 12` | `gh pr checks 12 --json name,state,bucket` |
|
|
205
|
+
| failing checks | `gh pr checks 12 --json name,bucket --jq '.[] \| select(.bucket == "fail") \| .name'` |
|
|
206
|
+
| `gh pr checks 12 --watch` | `gh run watch <run-id> --exit-status`, or poll `gh pr checks 12 --json bucket --jq '[.[] \| select(.bucket == "pending")] \| length'` |
|
|
207
|
+
| `gh api repos/o/r/pulls/12` | `gh api repos/o/r/pulls/12 --jq .head.sha` |
|
|
208
|
+
| `gh api -X PATCH repos/o/r/issues/12 -f state=closed` | the same with `--silent` |
|
|
209
|
+
|
|
210
|
+
gh refuses `--watch` together with `--json`, so a scalar watch is a polling
|
|
211
|
+
loop or `gh run watch`.
|
|
212
|
+
|
|
213
|
+
## Limits
|
|
214
|
+
|
|
215
|
+
- It judges the command text. A script file, a shell alias or function
|
|
216
|
+
defined in an earlier command or a startup file, a program that runs `gh`
|
|
217
|
+
itself (Python, Node, Perl, awk, `make`, a git alias) and a `gh` extension's
|
|
218
|
+
own output are not visible to it.
|
|
219
|
+
- `GH_DEBUG` set in the harness's own environment, rather than in the
|
|
220
|
+
command, is not visible to it.
|
|
221
|
+
- `curl`, `wget` and other HTTP clients calling `api.github.com` directly,
|
|
222
|
+
and `git log` or `git show` of a commit message, are outside its scope.
|
|
223
|
+
- It is not the only exposure path: workflow logs, `gh pr diff`, file
|
|
224
|
+
contents and other tools can print a link that someone wrote there. The
|
|
225
|
+
optional `signed-link-output` PostToolUse hook is the backstop for output.
|
|
226
|
+
- Codex's interactive terminal input (`write_stdin` into a running unified
|
|
227
|
+
exec session) has not been verified to pass through PreToolUse; a command
|
|
228
|
+
typed into a running shell that way may not be judged.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signed-link-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PreToolUse Signed Link Guard hook for @hasna/hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/hook.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"author": "Hasna",
|
|
11
|
+
"license": "Apache-2.0"
|
|
12
|
+
}
|