hookgate 0.0.2
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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +20 -0
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/hookgate.mjs +109 -0
- package/hooks/hooks.json +32 -0
- package/package.json +48 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookgate",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Allan Nava",
|
|
5
|
+
"url": "https://github.com/Allan-Nava"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "Calibrated decision gates for Claude Code hooks.",
|
|
9
|
+
"version": "0.0.2"
|
|
10
|
+
},
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "hookgate",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"description": "PreToolUse and Stop gates decided by TypeSafe's Jev with calibrated confidence — allow, ask or deny in about 100 ms, fail-open when the API is unreachable."
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookgate",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Calibrated, sub-second decisions in Claude Code's hooks: a PreToolUse gate on shell commands and a Stop gate on unverified completion, answered by TypeSafe's Jev with a confidence score, escalating to the human when unsure.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Allan Nava",
|
|
7
|
+
"url": "https://github.com/Allan-Nava"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Allan-Nava/hookgate",
|
|
10
|
+
"repository": "https://github.com/Allan-Nava/hookgate",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"hooks",
|
|
14
|
+
"guardrails",
|
|
15
|
+
"typesafe",
|
|
16
|
+
"system-one",
|
|
17
|
+
"calibrated-confidence",
|
|
18
|
+
"agent-safety"
|
|
19
|
+
]
|
|
20
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allan Nava
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Allan-Nava/hookgate/main/assets/logo.svg" width="72" height="72" alt="hookgate">
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://allan-nava.github.io/hookgate/"><img src="https://img.shields.io/badge/docs-allan--nava.github.io%2Fhookgate-2f5d8a?labelColor=1b1a18" alt="Documentation"></a>
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2f5d8a?labelColor=1b1a18" alt="MIT licence"></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# hookgate
|
|
10
|
+
|
|
11
|
+
**Calibrated, sub-second decisions inside Claude Code's hooks.** A `PreToolUse` gate
|
|
12
|
+
on shell commands and a `Stop` gate on unverified claims of completion, answered by
|
|
13
|
+
[TypeSafe's Jev](https://typesafe.ai/) — a model that returns typed decisions with a
|
|
14
|
+
confidence score instead of text — in about 100 ms. Confident: act. Unsure: ask the
|
|
15
|
+
human. Unreachable: get out of the way.
|
|
16
|
+
|
|
17
|
+
> **Status: scaffold.** The plugin installs and does nothing yet. The two gates are
|
|
18
|
+
> being designed in `thoughts/HG-1-jev-gates/` through the
|
|
19
|
+
> [QRSPI](https://github.com/Allan-Nava/qrspi) workflow; the first release ships them
|
|
20
|
+
> with the benchmark below filled in. Until then every handler falls through.
|
|
21
|
+
|
|
22
|
+
## Why in the hooks
|
|
23
|
+
|
|
24
|
+
A coding agent's harness takes hundreds of small decisions per session: is this
|
|
25
|
+
`rm -rf` aimed at the repo or at `~`, does "done, all tests pass" match `git status`,
|
|
26
|
+
should this be allowed, asked about or refused. Today those are taken by a static
|
|
27
|
+
allowlist, by a full LLM call (`type: prompt` hooks — seconds and cents each), or by
|
|
28
|
+
nobody. A System One model is built for exactly this shape: unstructured state in,
|
|
29
|
+
a typed answer plus a calibrated probability out, no text to parse, no hallucination
|
|
30
|
+
to catch. TypeSafe's own [plugin](https://github.com/typesafe-ai/skills) and
|
|
31
|
+
[cookbooks](https://docs.typesafe.ai/cookbooks.md) show how to build *applications*
|
|
32
|
+
on Jev. Nobody has put it inside the agent harness. That is the gap this fills.
|
|
33
|
+
|
|
34
|
+
## What ships
|
|
35
|
+
|
|
36
|
+
| Gate | Hook | Question to Jev | Effect |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| Command risk | `PreToolUse` on `Bash` | `Choice{allow, ask, deny}` + `Noul` "destroys data or state outside the repo?" | `permissionDecision` with a reason; below the confidence threshold it is always `ask`, never `allow` |
|
|
39
|
+
| Unverified completion | `Stop` | `Noul` "does the last message claim a completion the visible state does not support?" on the message plus `git status` | `block` with the reason, so the agent verifies before stopping |
|
|
40
|
+
|
|
41
|
+
Thresholds live in `.claude/hookgate.json` and scale with risk, as
|
|
42
|
+
[TypeSafe's confidence guide](https://docs.typesafe.ai/confidence.md) recommends.
|
|
43
|
+
|
|
44
|
+
**Fail-open, always.** No `TYPESAFE_API_KEY`, no network, a timeout, a 5xx or a bug
|
|
45
|
+
in this plugin means *no decision*: exit 0, empty stdout, and Claude Code's normal
|
|
46
|
+
permission flow applies as if hookgate were not installed. A gate that stalls the
|
|
47
|
+
agent is worse than none. Fail-closed will be an explicit opt-in.
|
|
48
|
+
|
|
49
|
+
**State never carries secrets.** Commands can contain tokens; anything that looks
|
|
50
|
+
like one is redacted before it leaves the machine, and state is truncated to Jev's
|
|
51
|
+
32k-token limit.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
Not published yet. Once it is:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
/plugin marketplace add Allan-Nava/hookgate
|
|
59
|
+
/plugin install hookgate@hookgate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
and `TYPESAFE_API_KEY` in the environment Claude Code runs in. Zero dependencies,
|
|
63
|
+
Node 18 or later, one `fetch` to `POST https://api.typesafe.ai/v1/systemone`.
|
|
64
|
+
|
|
65
|
+
## Benchmark
|
|
66
|
+
|
|
67
|
+
The first release carries, in `evals/`, at least fifty shell commands labelled by
|
|
68
|
+
hand as safe, ask or dangerous, run against both gates and against a `type: prompt`
|
|
69
|
+
hook on `claude-opus-5`, and reports agreement with the labels, p50 and p95 latency,
|
|
70
|
+
cost per decision and the share of `ask` per confidence threshold. One run, one
|
|
71
|
+
model version, dated — a data point, not a benchmark suite. If agreement stays under
|
|
72
|
+
about 90%, the command gate ships `ask`-only.
|
|
73
|
+
|
|
74
|
+
## Two design notes
|
|
75
|
+
|
|
76
|
+
**Why not teach Jev?** TypeSafe already does, well: official Python and JavaScript
|
|
77
|
+
SDKs, an MIT plugin with the patterns, thirteen cookbooks. Duplicating that would be
|
|
78
|
+
noise. hookgate has one job the harness can feel.
|
|
79
|
+
|
|
80
|
+
**Why zero dependencies?** A hook starts on every tool call. Start-up cost *is* the
|
|
81
|
+
cost, so there is nothing to install, nothing to resolve, one file to read.
|
|
82
|
+
|
|
83
|
+
## Prior art
|
|
84
|
+
|
|
85
|
+
- [typesafe-ai/skills](https://github.com/typesafe-ai/skills) — the official agent
|
|
86
|
+
skill: API, primitives, patterns. Read it to build with Jev.
|
|
87
|
+
- [Claude Code hooks](https://code.claude.com/docs/en/hooks) — `type: prompt` hooks
|
|
88
|
+
are the incumbent for judged decisions; this is the faster, calibrated alternative.
|
|
89
|
+
- [qrspi](https://github.com/Allan-Nava/qrspi) — the sibling project this one is
|
|
90
|
+
modelled on: a Markdown-first plugin, a dependency-free installer, a site generated
|
|
91
|
+
from the README, releases by tag.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT.
|
package/bin/hookgate.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hookgate — calibrated decision gates for Claude Code hooks.
|
|
3
|
+
//
|
|
4
|
+
// hookgate check validate the manifests, hooks.json and this file
|
|
5
|
+
// hookgate pre-tool-use PreToolUse handler (reads the hook JSON on stdin)
|
|
6
|
+
// hookgate stop Stop handler (reads the hook JSON on stdin)
|
|
7
|
+
// hookgate help
|
|
8
|
+
//
|
|
9
|
+
// Every handler FAILS OPEN. Exit 0 with no JSON on stdout means "no decision":
|
|
10
|
+
// Claude Code's ordinary permission flow applies, exactly as if the plugin were not
|
|
11
|
+
// installed. A missing TYPESAFE_API_KEY, a timeout, a 5xx or a bug in this file must
|
|
12
|
+
// never block the user's work. Fail-closed is an explicit opt-in for later.
|
|
13
|
+
//
|
|
14
|
+
// Zero dependencies, Node 18+: a hook starts on every tool call, so start-up cost
|
|
15
|
+
// is the cost. The API is one POST with fetch, which Node 18 has.
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
22
|
+
const read = (p) => readFileSync(join(ROOT, p), 'utf8')
|
|
23
|
+
const json = (p) => JSON.parse(read(p))
|
|
24
|
+
|
|
25
|
+
const [cmd = 'help'] = process.argv.slice(2)
|
|
26
|
+
|
|
27
|
+
// --- hook handlers ----------------------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// Not implemented yet: the gates are being designed under thoughts/HG-1-jev-gates/
|
|
30
|
+
// through the QRSPI workflow. Until then both handlers fall through — the plugin is
|
|
31
|
+
// installable and inert, which is what fail-open means.
|
|
32
|
+
|
|
33
|
+
async function readStdin() {
|
|
34
|
+
let s = ''
|
|
35
|
+
for await (const chunk of process.stdin) s += chunk
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(s)
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handler(name) {
|
|
44
|
+
const input = await readStdin()
|
|
45
|
+
if (!input || !process.env.TYPESAFE_API_KEY) process.exit(0)
|
|
46
|
+
process.stderr.write(`hookgate: ${name} gate not implemented yet — falling through\n`)
|
|
47
|
+
process.exit(0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- check ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
const HOOK_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop', 'UserPromptSubmit', 'PermissionRequest'])
|
|
53
|
+
|
|
54
|
+
function check() {
|
|
55
|
+
const errors = []
|
|
56
|
+
const fail = (m) => errors.push(m)
|
|
57
|
+
|
|
58
|
+
const pkg = json('package.json')
|
|
59
|
+
const plugin = json('.claude-plugin/plugin.json')
|
|
60
|
+
const market = json('.claude-plugin/marketplace.json')
|
|
61
|
+
|
|
62
|
+
const versions = { 'package.json': pkg.version, 'plugin.json': plugin.version, 'marketplace.json': market.metadata?.version }
|
|
63
|
+
if (new Set(Object.values(versions)).size !== 1) fail(`versions differ: ${JSON.stringify(versions)}`)
|
|
64
|
+
if (pkg.name !== 'hookgate' || plugin.name !== 'hookgate') fail('package.json and plugin.json must both be named hookgate')
|
|
65
|
+
if (!(market.plugins ?? []).some((p) => p.name === 'hookgate' && p.source === './')) fail('marketplace.json must list the hookgate plugin with source "./"')
|
|
66
|
+
if (!/github\.com[/:]Allan-Nava\/hookgate/.test(pkg.repository?.url ?? '')) fail('package.json#repository must name the GitHub repo')
|
|
67
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length) fail('no runtime dependencies — a hook runs on every tool call')
|
|
68
|
+
for (const f of ['bin', 'hooks', '.claude-plugin', 'README.md', 'LICENSE']) if (!pkg.files?.includes(f)) fail(`package.json#files is missing ${f}`)
|
|
69
|
+
|
|
70
|
+
const hooks = json('hooks/hooks.json')
|
|
71
|
+
for (const [event, entries] of Object.entries(hooks.hooks ?? {})) {
|
|
72
|
+
if (!HOOK_EVENTS.has(event)) fail(`hooks.json: unknown event ${event}`)
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
for (const h of entry.hooks ?? []) {
|
|
75
|
+
if (h.type !== 'command') fail(`${event}: only command hooks are used here (got ${h.type})`)
|
|
76
|
+
if (h.command !== '${CLAUDE_PLUGIN_ROOT}/bin/hookgate.mjs') fail(`${event}: command must be \${CLAUDE_PLUGIN_ROOT}/bin/hookgate.mjs, got ${h.command}`)
|
|
77
|
+
if (!Array.isArray(h.args) || !['pre-tool-use', 'stop'].includes(h.args[0])) fail(`${event}: args must name a handler this file implements`)
|
|
78
|
+
if (typeof h.timeout !== 'number' || h.timeout > 10) fail(`${event}: timeout must be set and at most 10 s — a gate that stalls the agent is worse than none`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!hooks.hooks?.PreToolUse?.some((e) => e.matcher === 'Bash')) fail('hooks.json: the PreToolUse gate must match Bash')
|
|
83
|
+
|
|
84
|
+
for (const f of ['README.md', 'CONTRIBUTING.md', 'CLAUDE.md', 'LICENSE']) if (!existsSync(join(ROOT, f))) fail(`${f} is missing`)
|
|
85
|
+
if (!/fail-open|fails open/i.test(read('README.md'))) fail('README.md must state the fail-open rule')
|
|
86
|
+
|
|
87
|
+
if (errors.length) {
|
|
88
|
+
for (const e of errors) console.error(`✗ ${e}`)
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
const n = Object.values(hooks.hooks).reduce((a, es) => a + es.reduce((b, e) => b + e.hooks.length, 0), 0)
|
|
92
|
+
console.log(`ok — ${n} hooks, manifests in sync at ${pkg.version}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function help() {
|
|
96
|
+
console.log(read('bin/hookgate.mjs').split('\n').slice(1, 6).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (cmd) {
|
|
100
|
+
case 'check':
|
|
101
|
+
check()
|
|
102
|
+
break
|
|
103
|
+
case 'pre-tool-use':
|
|
104
|
+
case 'stop':
|
|
105
|
+
await handler(cmd)
|
|
106
|
+
break
|
|
107
|
+
default:
|
|
108
|
+
help()
|
|
109
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "hookgate — calibrated gates. Every handler fails open: no key, no network or an API error means no decision, and Claude Code's normal permission flow takes over.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/bin/hookgate.mjs",
|
|
11
|
+
"args": ["pre-tool-use"],
|
|
12
|
+
"timeout": 5,
|
|
13
|
+
"statusMessage": "hookgate: judging the command"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"Stop": [
|
|
19
|
+
{
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/bin/hookgate.mjs",
|
|
24
|
+
"args": ["stop"],
|
|
25
|
+
"timeout": 5,
|
|
26
|
+
"statusMessage": "hookgate: checking the claim of completion"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookgate",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Calibrated, sub-second decisions in Claude Code's hooks: a PreToolUse gate on shell commands and a Stop gate on unverified completion, answered by TypeSafe's Jev with a confidence score, escalating to the human when unsure.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hookgate": "bin/hookgate.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"hooks",
|
|
12
|
+
".claude-plugin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build:site": "node site/build.mjs",
|
|
21
|
+
"test": "node bin/hookgate.mjs check",
|
|
22
|
+
"backlog": "node scripts/backlog.mjs check && node scripts/backlog_test.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"claude-code-plugin",
|
|
28
|
+
"hooks",
|
|
29
|
+
"guardrails",
|
|
30
|
+
"typesafe",
|
|
31
|
+
"system-one",
|
|
32
|
+
"calibrated-confidence",
|
|
33
|
+
"agent-safety"
|
|
34
|
+
],
|
|
35
|
+
"author": "Allan Nava (https://github.com/Allan-Nava)",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://github.com/Allan-Nava/hookgate",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Allan-Nava/hookgate.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Allan-Nava/hookgate/issues"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"marked": "^18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|