agent-base-kit 0.1.0 → 0.1.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 (2) hide show
  1. package/README.md +49 -162
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -4,228 +4,124 @@
4
4
 
5
5
  # agent-base-kit
6
6
 
7
- **Turn any check into a fail-closed PreToolUse hook for Claude Code and Codex. One function in, a verdict out. Zero dependencies.**
7
+ **The base kit for Claude Code and Codex hooks.**
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/agent-base-kit?color=cb3837)](https://www.npmjs.com/package/agent-base-kit)
10
10
  [![npm downloads](https://img.shields.io/npm/dm/agent-base-kit)](https://www.npmjs.com/package/agent-base-kit)
11
- [![CI](https://github.com/devmaxxx/agent-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/devmaxxx/agent-kit/actions/workflows/ci.yml)
11
+ [![CI](https://github.com/devmaxxx/agent-base-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/devmaxxx/agent-base-kit/actions/workflows/ci.yml)
12
12
  [![types](https://img.shields.io/npm/types/agent-base-kit)](https://www.npmjs.com/package/agent-base-kit)
13
13
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](./package.json)
14
14
  [![node](https://img.shields.io/node/v/agent-base-kit)](./package.json)
15
15
  [![license: MIT](https://img.shields.io/npm/l/agent-base-kit)](./LICENSE)
16
16
 
17
- [Install](#install) · [Quick start](#quick-start) · [Kinds](#kinds-and-matchers) · [Fail-closed](#the-fail-closed-contract) · [Install layout](#what-install-writes) · [Claude Code plugin](#shipping-as-a-claude-code-plugin) · [API](#api)
17
+ [Why](#why) · [What you get](#what-you-get) · [Usage](#usage) · [What install writes](#what-install-writes) · [Fail-closed](#the-fail-closed-contract) · [Kinds](#kinds-and-matchers) · [API](#api)
18
18
 
19
19
  </div>
20
20
 
21
- <details>
22
- <summary><b>Table of contents</b></summary>
23
-
24
- - [Why](#why)
25
- - [Features](#features)
26
- - [Install](#install)
27
- - [Quick start](#quick-start)
28
- - [Why a devDependency](#why-a-devdependency)
29
- - [Kinds and matchers](#kinds-and-matchers)
30
- - [The fail-closed contract](#the-fail-closed-contract)
31
- - [What install writes](#what-install-writes)
32
- - [Shipping as a Claude Code plugin](#shipping-as-a-claude-code-plugin)
33
- - [API](#api)
34
- - [Design notes](#design-notes)
35
- - [Contributing](#contributing)
36
- - [License](#license)
37
-
38
- </details>
39
-
40
21
  ## Why
41
22
 
42
- A [PreToolUse hook](https://code.claude.com/docs/en/hooks) is the last point where you can stop an agent's tool call before it runs. Writing one well is mostly not about the check. The agents treat every exit except 2 as "go on", so a hook that crashes, cannot parse its input, or loses its Node.js after an upgrade lets every call through without a word.
23
+ A [PreToolUse hook](https://code.claude.com/docs/en/hooks) is the last point where you can stop an agent's tool call. Every hook needs the same plumbing before its own check: read the agent's payload, write the agent's config without breaking anyone else's entries, keep running across Node.js upgrades, and block when the check cannot run, since the agents treat every exit except 2 as "go on".
43
24
 
44
- agent-base-kit is that plumbing, for any guard you build. You write the check; it makes sure the check actually runs, and blocks when it cannot.
25
+ agent-base-kit is that plumbing, shared by every hook you build. You write the check; reuse the rest for a secret scanner, a policy guard, or any other hook.
45
26
 
46
- ## Features
27
+ ## What you get
47
28
 
48
- - **One function.** `check(call)` gets a typed call (`shell`, `write`, `edit` or `patch`) and returns `{ block, reason }`. Sync or async.
49
- - **Fails closed.** Unreadable payloads, throws, bad verdicts, hangs and a hook that cannot start all block.
50
- - **Both agents.** Claude Code and Codex, user or project scope, with matchers derived from the kinds you list.
51
- - **Survives upgrades.** A user install runs its own copy of your build on the Node.js that installed it, so an nvm or Homebrew upgrade does not silently switch it off.
52
- - **Plays well with other hooks.** Install and uninstall touch only their own entry and refuse a config they cannot extend safely.
29
+ - **One function to write.** `check(call)` gets a typed call (`shell`, `write`, `edit` or `patch`) and returns `{ block, reason }`, sync or async.
30
+ - **Both agents.** Claude Code and Codex, with matchers derived from the kinds you list.
31
+ - **Fail-closed by default.** Unreadable payloads, throws, bad verdicts, hangs and a hook that cannot start all block.
32
+ - **One command to install.** Your bin gets `install` and `uninstall` for `claude` or `codex`, at user or project scope.
33
+ - **Other hooks left alone.** Install and uninstall touch only their own entry, and refuse a config they cannot extend safely.
34
+ - **Survives upgrades.** A user install runs its own copy of your build on the Node.js that installed it, so an nvm or Homebrew upgrade or a rebuild does not switch it off.
53
35
  - **Claude Code plugin files.** One call writes the manifest, `hooks.json` and a marketplace.
54
36
  - **Zero dependencies**, ESM only, typed.
55
37
 
56
- ## Install
38
+ ## Usage
57
39
 
58
40
  ```sh
59
41
  pnpm add -D agent-base-kit
60
- # or: npm i -D agent-base-kit · yarn add -D agent-base-kit · bun add -d agent-base-kit
61
42
  ```
62
43
 
63
- Requires Node.js 22 or later and a POSIX shell. It goes in `devDependencies`; [here is why](#why-a-devdependency).
64
-
65
- ## Quick start
66
-
67
- A toy check that stops force pushes, as a plugin in three files:
44
+ Write the check and hand it to `runCli` in your bin:
68
45
 
69
46
  ```ts
70
- // src/plugin.ts
71
- import { defineHook } from 'agent-base-kit'
47
+ #!/usr/bin/env node
48
+ import { defineHook, runCli } from 'agent-base-kit'
72
49
 
73
- export const plugin = defineHook({
50
+ const plugin = defineHook({
74
51
  name: 'no-force-push',
75
52
  kinds: ['shell'],
76
- statusMessage: 'Checking for force pushes',
77
53
  check: call => /\bgit\s+push\b.*\s(?:-f|--force)(?:\s|$)/.test(call.command)
78
54
  ? { block: true, reason: 'Blocked by no-force-push: push without --force, or use --force-with-lease.' }
79
55
  : { block: false },
80
56
  })
81
- ```
82
-
83
- ```ts
84
- #!/usr/bin/env node
85
- // src/cli.ts
86
- import { runCli } from 'agent-base-kit'
87
- import { plugin } from './plugin'
88
57
 
89
58
  void runCli(plugin, import.meta.url)
90
59
  ```
91
60
 
92
- ```jsonc
93
- // package.json (the parts that matter)
94
- {
95
- "name": "no-force-push",
96
- "type": "module",
97
- "bin": { "no-force-push": "./dist/cli.mjs" },
98
- "files": ["dist"],
99
- "engines": { "node": ">=22" },
100
- "devDependencies": {
101
- "agent-base-kit": "^0.1.0",
102
- "tsdown": "^0.23.1"
103
- }
104
- }
105
- ```
106
-
107
- Build `src/cli.ts` with tsdown. That gives you this CLI:
61
+ Bundle the bin into `.mjs` files with agent-base-kit inlined, for example with tsdown, since the stable copy has no `node_modules`. Your users then run:
108
62
 
109
63
  ```sh
110
64
  npx no-force-push install claude # ~/.claude/settings.json
111
65
  npx no-force-push install codex --project # ./.codex/hooks.json
112
66
  npx no-force-push uninstall claude
113
- npx no-force-push hook # what the agent runs, with the tool call on stdin
114
67
  ```
115
68
 
116
- ### Why a devDependency
69
+ Requires Node.js 22 or later and a POSIX shell.
117
70
 
118
- A user-scope install copies the `.mjs` files next to your bin into `~/.<name>/<build-id>/` and runs them from there. A runtime dependency would be imported from `node_modules`, and the copy has none. List agent-base-kit under `devDependencies` instead, and tsdown bundles it into your build. It also inlines its types into your `.d.mts`, so `export type { ToolCall } from 'agent-base-kit'` works for your users. If the copy cannot start anyway, `install` refuses and says why. It runs the copy once before writing any config.
71
+ ## What install writes
119
72
 
120
- ## Kinds and matchers
73
+ A user install (the default scope) does four things:
121
74
 
122
- `kinds` decides which calls reach your check. Each agent's matcher is derived from it.
75
+ 1. **Copies the build.** The `.mjs` files beside your bin go to `~/.<name>/<build-id>/`, where the id is a hash of their content. The same build reuses its directory, a new build gets a new one, and the copy is staged and renamed in, so a hook never loads a half-written one.
76
+ 2. **Runs the copy once.** Before any config points at it, the copy runs on a tool no kind covers. If it cannot start, `install` refuses and changes nothing.
77
+ 3. **Swaps `~/.<name>/current`.** One rename moves the link to the new build, so the hook command stays the same across versions and Codex keeps trusting it.
78
+ 4. **Writes the hook:**
123
79
 
124
- | Kind | The call your check gets | Claude Code tools | Codex tools |
125
- | --- | --- | --- | --- |
126
- | `shell` | `{ kind: 'shell', command }` | `Bash` | `Bash` |
127
- | `write` | `{ kind: 'write', path, content }` | `Write`, `NotebookEdit` | `apply_patch` |
128
- | `edit` | `{ kind: 'edit', path, before, after }` (a MultiEdit's edits joined by newlines) | `Edit`, `MultiEdit` | `apply_patch` |
129
- | `patch` | `{ kind: 'patch', patch }` (the whole patch text) | none | `apply_patch` |
80
+ ```sh
81
+ n='/path/to/the/installing/node'; [ -x "$n" ] || n=node; "$n" '/home/you/.<name>/current/cli.mjs' hook || exit 2
82
+ ```
130
83
 
131
- `check` is typed to the kinds you list. It is never called with any other kind, even if a hand-written matcher sends one.
84
+ `--project` writes `npx --no-install <packageName> hook || exit 2` instead, which is safe to commit. Uninstall removes the entry and leaves the copy, since another agent may still run it.
132
85
 
133
- Codex sends every file change as one `apply_patch`. A plugin whose kinds include `write` or `edit` but not `patch` would match those calls and then never see them. So `install codex` refuses that plugin until you add `patch` and handle the patch text. A plugin with no tool in an agent, such as `['patch']` for Claude Code, is refused the same way.
86
+ Install replaces any entry it wrote before and keeps every other one. It refuses, and changes nothing, when the settings file is not JSON or not an object with a `hooks` object and a `PreToolUse` list, or when `~/.<name>/current` exists and is not a link.
134
87
 
135
88
  ## The fail-closed contract
136
89
 
137
- The agents treat exit 2 as "block" and every other exit as "go on". So the hook exits 0 only when your check passes the call:
90
+ The hook exits 0 only when your check passes the call:
138
91
 
139
92
  | What happens | Exit |
140
93
  | --- | --- |
141
94
  | `check` returns `{ block: false }` | 0 |
142
95
  | `check` returns `{ block: true, reason }` (the reason goes to stderr as is) | 2 |
143
- | `check` throws or rejects | 2 |
144
- | `check` returns anything else: `undefined`, `{ block: true }` without a reason, a string | 2 |
145
- | `check` returns a promise that never settles | 2 |
146
- | The payload is not JSON, not an object, names no tool, or has a field of the wrong shape (a `command` sent as an array, `edits` as an object) | 2 |
96
+ | `check` throws, rejects, never settles, or returns anything else | 2 |
97
+ | The payload is not JSON, names no tool, or has a field of the wrong shape | 2 |
147
98
  | The tool is not one of your kinds | 0, and `check` is not called |
148
99
  | The hook cannot start: Node.js is missing, the build was deleted, a dependency was not bundled | 2, from `\|\| exit 2` |
149
100
  | The hook runs on a Node.js older than 22 | 2, with a message naming the fix |
150
- | The hook times out | Claude Code blocks it (`onFailure: "block"`). Codex lets the call run. |
151
-
152
- `check` may be async. Keep it well inside `timeout`, which defaults to 10 seconds. Past the timeout Codex runs the call unchecked.
153
-
154
- ## What install writes
155
-
156
- The user scope is the default. Install copies the build to `~/.<name>/<build-id>/` and swaps the `~/.<name>/current` link onto it with a rename. Then it writes:
157
-
158
- ```sh
159
- n='/path/to/the/installing/node'; [ -x "$n" ] || n=node; "$n" '/home/you/.<name>/current/cli.mjs' hook || exit 2
160
- ```
161
-
162
- - The hook keeps running on the Node.js that installed it, so a project that pins an older Node does not change it.
163
- - If that Node.js is removed, the hook falls back to the `node` on PATH.
164
- - The copy outlives the package. An nvm upgrade or an `npx` cache clean does not remove it.
165
- - The command stays the same across versions installed by the same Node.js, so Codex keeps trusting it.
166
-
167
- `--project` writes `npx --no-install <packageName> hook || exit 2`, which is safe to commit.
168
-
169
- Install replaces any entry it wrote before, including the older forms listed below, and keeps every other entry as it is. It refuses, and changes nothing, when:
170
-
171
- - the settings file is not JSON, or not an object with a `hooks` object and a `PreToolUse` list (a `null` there counts as absent);
172
- - `~/.<name>/current` exists and is not a link;
173
- - the bin is not a built `.mjs` file;
174
- - the copied build does not start.
175
-
176
- Uninstall leaves the copy in place, since another agent may still run it.
177
-
178
- In Claude Code the entry also sets `"onFailure": "block"`. That needs Claude Code 2.1.295 or later. Codex has no such field.
101
+ | The hook times out | Claude Code blocks it (`onFailure: "block"`, Claude Code 2.1.295 or later). Codex lets the call run. |
179
102
 
180
- ## Shipping as a Claude Code plugin
103
+ Keep `check` well inside `timeout`, which defaults to 10 seconds.
181
104
 
182
- A Claude Code plugin needs no install step and no copy, because Claude Code keeps its own copy of the plugin. Generate the plugin files from your build, for example in tsdown's `onSuccess`:
183
-
184
- ```ts
185
- // tsdown.config.ts
186
- import { writeClaudePlugin } from 'agent-base-kit'
187
- import { defineConfig } from 'tsdown'
188
- import pkg from './package.json' with { type: 'json' }
189
- import { plugin } from './src/plugin'
190
-
191
- export default defineConfig({
192
- entry: ['src/cli.ts'],
193
- onSuccess() {
194
- writeClaudePlugin(import.meta.dirname, {
195
- plugin,
196
- entry: 'dist/cli.mjs',
197
- version: pkg.version,
198
- description: 'Stops force pushes',
199
- author: { name: 'you' },
200
- license: 'MIT',
201
- marketplace: { name: 'no-force-push', owner: { name: 'you' } },
202
- })
203
- },
204
- })
205
- ```
206
-
207
- This writes three files:
208
-
209
- - `.claude-plugin/plugin.json` holds the manifest.
210
- - `hooks/hooks.json` holds the hook: `node "${CLAUDE_PLUGIN_ROOT}/dist/cli.mjs" hook || exit 2`, with the matcher from `kinds`, your `timeout` and `statusMessage`, and `"onFailure": "block"`.
211
- - `.claude-plugin/marketplace.json` is written only with `marketplace`. It is a one-plugin marketplace whose source is your npm package.
105
+ ## Kinds and matchers
212
106
 
213
- The build is not committed to git, and Claude Code copies a plugin as it is, without building it. So the marketplace fetches the plugin from npm, where `dist` is published. Add `.claude-plugin` and `hooks` to `files` in package.json, publish, and commit `.claude-plugin/marketplace.json` to the repository. Users then run:
107
+ `kinds` decides which calls reach your check, and each agent's matcher is derived from it.
214
108
 
215
- ```text
216
- /plugin marketplace add you/no-force-push
217
- /plugin install no-force-push@no-force-push
218
- ```
109
+ | Kind | The call your check gets | Claude Code tools | Codex tools |
110
+ | --- | --- | --- | --- |
111
+ | `shell` | `{ kind: 'shell', command }` | `Bash` | `Bash` |
112
+ | `write` | `{ kind: 'write', path, content }` | `Write`, `NotebookEdit` | `apply_patch` |
113
+ | `edit` | `{ kind: 'edit', path, before, after }` (a MultiEdit's edits joined by newlines) | `Edit`, `MultiEdit` | `apply_patch` |
114
+ | `patch` | `{ kind: 'patch', patch }` (the whole patch text) | none | `apply_patch` |
219
115
 
220
- Use either the plugin or `install claude`, not both, or the check runs twice.
116
+ Codex sends every file change as one `apply_patch`, so `install codex` refuses a plugin with `write` or `edit` but no `patch`. A plugin with no tool in an agent, such as `['patch']` for Claude Code, is refused too.
221
117
 
222
118
  ## API
223
119
 
224
120
  ```ts
225
121
  function defineHook<const K extends ToolKind>(definition: HookDefinition<K>): HookPlugin
226
- function runHook(plugin: HookPlugin, stdin: string, options?: { cwd?: string, stderr?: (text: string) => void }): Promise<number>
227
- function createCli(plugin: HookPlugin, options?: { bin?: string }): (argv: readonly string[], io: CliIO) => Promise<number>
228
122
  function runCli(plugin: HookPlugin, entryUrl: string, options?: { bin?: string }): Promise<void>
123
+ function createCli(plugin: HookPlugin, options?: { bin?: string }): (argv: readonly string[], io: CliIO) => Promise<number>
124
+ function runHook(plugin: HookPlugin, stdin: string, options?: { cwd?: string, stderr?: (text: string) => void }): Promise<number>
229
125
  function readStdin(stream?: AsyncIterable<unknown>): Promise<string>
230
126
  function install(options: InstallOptions): { file: string, command?: string }
231
127
  function uninstall(options: UninstallOptions): { file: string }
@@ -235,22 +131,13 @@ function claudePluginFiles(options: ClaudePluginOptions): Record<string, string>
235
131
  function writeClaudePlugin(dir: string, options: ClaudePluginOptions): string[]
236
132
  ```
237
133
 
238
- `install`, `uninstall` and `createCli` take the home directory, the working directory, the bin path and the Node.js path as arguments, and never read them from the process. `runCli` is the one function that does, which keeps tests away from your real `~`.
239
-
240
- ## Design notes
241
-
242
- - **Async checks.** A sync-only API would have to break later to allow I/O. A promise costs nothing now.
243
- - **The reason is printed as is.** Your plugin owns its message, so start it with the plugin's name. Errors get a `Blocked by <name>, it could not check the call: …` prefix.
244
- - **Which entries count as its own.** The `npx` form must match exactly. The pinned form matches any file under `~/.<name>/current/`, so a bin renamed between releases still replaces its old entry. The bare `node '<path>' hook` form from before the copy existed needs a directory named exactly after the plugin. So a hook that only mentions the plugin, or another plugin whose path merely contains this one's name, is never removed.
245
- - **POSIX shells only.** Hook commands rely on `|| exit 2` and single-quoted paths, so Windows is not supported yet.
134
+ `runCli` is the only function that reads the home directory, the working directory and the Node.js path from the process; the others take them as arguments, which keeps tests away from your real `~`. `writeClaudePlugin` writes the files to ship the same hook as a Claude Code plugin instead; use either the plugin or `install claude`, not both.
246
135
 
247
136
  ## Contributing
248
137
 
249
- Issues and pull requests are welcome.
250
-
251
138
  ```sh
252
- git clone https://github.com/devmaxxx/agent-kit.git
253
- cd agent-kit
139
+ git clone https://github.com/devmaxxx/agent-base-kit.git
140
+ cd agent-base-kit
254
141
  pnpm install
255
142
  pnpm lint && pnpm typecheck && pnpm test
256
143
  ```
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "agent-base-kit",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "packageManager": "pnpm@10.23.0",
6
- "description": "Turn any check into a fail-closed PreToolUse hook for Claude Code and Codex: tool-call parsing, install and uninstall, a stable copy of the build, and Claude Code plugin files. Zero dependencies.",
6
+ "description": "The base kit for Claude Code and Codex hooks: typed tool calls, a fail-closed runner, install and uninstall with a stable copy of the build, and Claude Code plugin files. Zero dependencies.",
7
7
  "author": "devmaxxx",
8
8
  "license": "MIT",
9
- "homepage": "https://github.com/devmaxxx/agent-kit#readme",
9
+ "homepage": "https://github.com/devmaxxx/agent-base-kit#readme",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/devmaxxx/agent-kit.git"
12
+ "url": "git+https://github.com/devmaxxx/agent-base-kit.git"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/devmaxxx/agent-kit/issues"
15
+ "url": "https://github.com/devmaxxx/agent-base-kit/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "ai-agent",