@datagusto/cli 0.0.0-stage → 0.2.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -2
  3. package/bin/run.js +5 -0
  4. package/dist/adapters/claude-code.d.ts +48 -0
  5. package/dist/adapters/claude-code.js +526 -0
  6. package/dist/adapters/claude-code.js.map +1 -0
  7. package/dist/adapters/claude-settings.d.ts +29 -0
  8. package/dist/adapters/claude-settings.js +115 -0
  9. package/dist/adapters/claude-settings.js.map +1 -0
  10. package/dist/adapters/index.d.ts +10 -0
  11. package/dist/adapters/index.js +37 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/kiro-agent.d.ts +29 -0
  14. package/dist/adapters/kiro-agent.js +124 -0
  15. package/dist/adapters/kiro-agent.js.map +1 -0
  16. package/dist/adapters/kiro.d.ts +24 -0
  17. package/dist/adapters/kiro.js +752 -0
  18. package/dist/adapters/kiro.js.map +1 -0
  19. package/dist/adapters/types.d.ts +81 -0
  20. package/dist/adapters/types.js +14 -0
  21. package/dist/adapters/types.js.map +1 -0
  22. package/dist/api/client.d.ts +36 -0
  23. package/dist/api/client.js +85 -0
  24. package/dist/api/client.js.map +1 -0
  25. package/dist/api/errors.d.ts +4 -0
  26. package/dist/api/errors.js +25 -0
  27. package/dist/api/errors.js.map +1 -0
  28. package/dist/commands/remove.d.ts +13 -0
  29. package/dist/commands/remove.js +59 -0
  30. package/dist/commands/remove.js.map +1 -0
  31. package/dist/commands/setup.d.ts +16 -0
  32. package/dist/commands/setup.js +114 -0
  33. package/dist/commands/setup.js.map +1 -0
  34. package/dist/commands/status.d.ts +7 -0
  35. package/dist/commands/status.js +53 -0
  36. package/dist/commands/status.js.map +1 -0
  37. package/dist/display.d.ts +2 -0
  38. package/dist/display.js +12 -0
  39. package/dist/display.js.map +1 -0
  40. package/dist/fs/atomic.d.ts +23 -0
  41. package/dist/fs/atomic.js +65 -0
  42. package/dist/fs/atomic.js.map +1 -0
  43. package/dist/key-input.d.ts +24 -0
  44. package/dist/key-input.js +96 -0
  45. package/dist/key-input.js.map +1 -0
  46. package/dist/platform.d.ts +24 -0
  47. package/dist/platform.js +61 -0
  48. package/dist/platform.js.map +1 -0
  49. package/dist/state/installs.d.ts +24 -0
  50. package/dist/state/installs.js +110 -0
  51. package/dist/state/installs.js.map +1 -0
  52. package/dist/vendor.d.ts +16 -0
  53. package/dist/vendor.js +60 -0
  54. package/dist/vendor.js.map +1 -0
  55. package/package.json +57 -4
  56. package/vendor/claude_code/.claude-plugin/plugin.json +55 -0
  57. package/vendor/claude_code/README.md +267 -0
  58. package/vendor/claude_code/dist/api-client.js +82 -0
  59. package/vendor/claude_code/dist/builtin-tools.generated.js +33 -0
  60. package/vendor/claude_code/dist/extractors.js +201 -0
  61. package/vendor/claude_code/dist/index.js +695 -0
  62. package/vendor/claude_code/dist/monitoring.js +66 -0
  63. package/vendor/claude_code/dist/state.js +71 -0
  64. package/vendor/claude_code/dist/types.js +5 -0
  65. package/vendor/claude_code/hooks/hooks.json +25 -0
  66. package/vendor/claude_code/package.json +26 -0
  67. package/vendor/kiro/.kiro/agents/kiro_default.json +20 -0
  68. package/vendor/kiro/.kiro/hooks/datagusto-post-tool-use.json +10 -0
  69. package/vendor/kiro/.kiro/hooks/datagusto-pre-tool-use.json +10 -0
  70. package/vendor/kiro/.kiro/hooks/datagusto-session-start.json +10 -0
  71. package/vendor/kiro/.kiro/hooks/datagusto-stop.json +10 -0
  72. package/vendor/kiro/.kiro/hooks/datagusto-user-prompt-submit.json +10 -0
  73. package/vendor/kiro/dist/api-client.js +82 -0
  74. package/vendor/kiro/dist/builtin-tools.generated.js +87 -0
  75. package/vendor/kiro/dist/engine.js +65 -0
  76. package/vendor/kiro/dist/extractors.js +201 -0
  77. package/vendor/kiro/dist/index.js +735 -0
  78. package/vendor/kiro/dist/monitoring.js +66 -0
  79. package/vendor/kiro/dist/state.js +71 -0
  80. package/vendor/kiro/dist/types.js +5 -0
  81. package/vendor/kiro/package.json +25 -0
  82. package/vendor/manifest.json +42 -0
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "datagusto-safety-plugin",
3
+ "description": "Integrates Datagusto safety validation API with Claude Code's hook lifecycle (PreToolUse / PermissionRequest / PostToolUse / PostToolUseFailure / UserPromptSubmit / SessionStart / SessionEnd).",
4
+ "version": "1.0.0",
5
+ "author": { "name": "Datagusto" },
6
+ "userConfig": {
7
+ "server_url": {
8
+ "type": "string",
9
+ "title": "Datagusto server URL",
10
+ "description": "Datagusto server base URL, e.g. https://datagusto.example.com. Leave empty if you connected with `dg setup claude-code`, which writes it to claude-code.json."
11
+ },
12
+ "api_key": {
13
+ "type": "string",
14
+ "title": "Datagusto API key",
15
+ "description": "Bearer auth key for the Datagusto API. Leave empty if you connected with `dg setup claude-code`, which writes it to claude-code.json.",
16
+ "sensitive": true
17
+ },
18
+ "timeout_ms": {
19
+ "type": "number",
20
+ "title": "Request timeout (ms)",
21
+ "description": "Per-request timeout for Datagusto API calls.",
22
+ "default": 30000
23
+ },
24
+ "fail_closed": {
25
+ "type": "boolean",
26
+ "title": "Fail closed on API errors",
27
+ "description": "Deny/block tool execution when the API is unreachable or errors.",
28
+ "default": true
29
+ },
30
+ "verbose": {
31
+ "type": "boolean",
32
+ "title": "Verbose logging",
33
+ "description": "Per-hook debug logging to stderr.",
34
+ "default": false
35
+ },
36
+ "monitor_only": {
37
+ "type": "boolean",
38
+ "title": "Monitor-only mode",
39
+ "description": "Log decisions without enforcing them.",
40
+ "default": false
41
+ },
42
+ "auto_approve_vetted_permissions": {
43
+ "type": "boolean",
44
+ "title": "Auto-approve vetted permissions",
45
+ "description": "When Datagusto's PreToolUse check already allowed a call, auto-resolve Claude Code's own native permission dialog instead of prompting the user again. Never overrides an explicit Datagusto ask/deny. Off by default — opt in to reduce double-prompting.",
46
+ "default": false
47
+ },
48
+ "allow_uncategorized_tools": {
49
+ "type": "boolean",
50
+ "title": "Allow uncategorized tools",
51
+ "description": "The alignment layer default-denies tools its intent-judge didn't predict from the prompt. When true, that specific deny is downgraded to allow if the assigned guardrail rules also allowed the call. Intent violations and assigned-rule blocks are never downgraded.",
52
+ "default": true
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,267 @@
1
+ # Datagusto Safety Plugin for Claude Code
2
+
3
+ A Claude Code plugin that calls [Datagusto](https://datagusto.com) safety validation APIs to enforce guardrails on agent tool execution.
4
+
5
+ This is the Claude Code port of the [OpenClaw plugin](../openclaw). Both plugins talk to the same Datagusto backend (the meta harness) and implement the same business semantics — proceed/block/ask/inject driven by the `/sessions/validate` response — but each maps that contract onto its own host framework's native hook vocabulary independently. See [Architecture](#architecture) and [Differences from the OpenClaw plugin](#differences-from-the-openclaw-plugin) for where the two diverge structurally.
6
+
7
+ ## Prerequisites
8
+
9
+ - Claude Code (or the Claude Agent SDK) with plugin support
10
+ - A running Datagusto server with a valid API key
11
+ - Node.js (to run the compiled hook script)
12
+
13
+ ## Installation
14
+
15
+ **With the `dg` CLI (recommended):** `dg setup claude-code` copies this plugin to `~/.datagusto/claude-code/`, writes `claude-code.json` there with your agent key, and adds the hooks to `~/.claude/settings.json` so every session runs them. See [`../cli`](../cli/README.md). The rest of this section is for developing the plugin itself.
16
+
17
+ **From source:**
18
+
19
+ ```sh
20
+ cd claude_code
21
+ npm install
22
+ npm run build
23
+ ```
24
+
25
+ This produces `dist/index.js`, the single compiled entrypoint every hook below invokes.
26
+
27
+ ### Loading the plugin
28
+
29
+ This directory is a real Claude Code plugin (`.claude-plugin/plugin.json` + `hooks/hooks.json`), not just a script you wire into your own `settings.json`. Load it directly off this built local directory:
30
+
31
+ **CLI (development):**
32
+ ```sh
33
+ claude --plugin-dir /absolute/path/to/claude_code
34
+ ```
35
+
36
+ **Agent SDK:**
37
+ ```ts
38
+ import { query } from "@anthropic-ai/claude-agent-sdk";
39
+
40
+ for await (const message of query({
41
+ prompt: "...",
42
+ options: { plugins: [{ type: "local", path: "/absolute/path/to/claude_code" }] },
43
+ })) { /* ... */ }
44
+ ```
45
+
46
+ Either path auto-discovers `.claude-plugin/plugin.json` and wires all seven hooks declared in `hooks/hooks.json` — there's nothing else to configure for loading. (Marketplace/npm distribution isn't set up here; both loading paths above work directly off a locally built directory.)
47
+
48
+ ## Testing (quickstart)
49
+
50
+ For local testing and demos, the most reliable place to put credentials is `claude-code.json` next to `package.json` (gitignored — safe for keys). `loadConfig()` reads it directly, so it works regardless of how the plugin host names its `CLAUDE_PLUGIN_OPTION_*` env vars.
51
+
52
+ 1. **Build:**
53
+ ```sh
54
+ npm install && npm run build
55
+ ```
56
+ 2. **Add credentials** — copy the template and fill in your real values:
57
+ ```sh
58
+ cp claude-code.json.example claude-code.json
59
+ # then edit claude-code.json: set serverUrl + apiKey
60
+ ```
61
+ ```json
62
+ {
63
+ "serverUrl": "http://localhost:8000",
64
+ "apiKey": "agt_live_your-key-here",
65
+ "verbose": true
66
+ }
67
+ ```
68
+ > The `apiKey` is the public API key of the **agent** on the Datagusto platform. The agent must exist with the identifier `claude_code` (the value the plugin sends as `agent_type`), under the account that owns this key.
69
+ 3. **Verify the connection** (no Claude session needed) — the register call should return `ok=true, status=200`:
70
+ ```sh
71
+ echo '{"hook_event_name":"SessionStart","session_id":"verify","source":"startup"}' | node dist/index.js
72
+ ```
73
+ 4. **Rehearse the guardrail behaviors** deterministically against the live backend:
74
+ ```sh
75
+ ./scripts/phase2-rehearsal.sh
76
+ ```
77
+ This drives synthetic Pre/Post payloads through the plugin and prints the exact decision Claude Code would act on for each scenario (allow / deny / ask / output-suppression / failed-tool). What each scenario produces depends on the guardrails assigned to the `claude_code` agent on the dashboard.
78
+ 5. **Launch a real session** from a scratch project:
79
+ ```sh
80
+ cd ~/some-scratch-project
81
+ claude --plugin-dir /absolute/path/to/claude_code
82
+ ```
83
+ With `verbose: true`, stderr shows the hook chain firing; the dashboard's Agents/Guardrail-Logs views show the calls landing.
84
+
85
+ ## Configuration
86
+
87
+ The options declared in `.claude-plugin/plugin.json`'s `userConfig` can be set through Claude Code (the `/plugin` install dialog or `/plugin configure`). `api_key` is marked `sensitive`, so it's stored in the OS keychain (or `~/.claude/.credentials.json`) rather than `settings.json`.
88
+
89
+ None of them is `required`, so Claude Code never blocks on a prompt for them. `dg setup claude-code` writes `serverUrl` and `apiKey` to `claude-code.json` instead, and that file is all the plugin needs. Values set through `userConfig` still take precedence over the file.
90
+
91
+ ### Configuration Options
92
+
93
+ | `userConfig` key | Type | Default | Description |
94
+ |---|------|---------|-------------|
95
+ | `server_url` | `string` | — | Datagusto server base URL. Needed from here, `claude-code.json` or `DATAGUSTO_SAFETY_SERVER_URL` |
96
+ | `api_key` | `string` | — | API key for Bearer authentication (sensitive — keychain-stored). Needed from here, `claude-code.json` or `DATAGUSTO_SAFETY_API_KEY` |
97
+ | `timeout_ms` | `number` | `30000` | Per-request timeout in milliseconds |
98
+ | `fail_closed` | `boolean` | `true` | Block tool execution when API calls fail |
99
+ | `verbose` | `boolean` | `false` | Enable hook-by-hook logging to stderr |
100
+ | `monitor_only` | `boolean` | `false` | Log deny/ask/block/inject/auto-approve decisions without enforcing them. OR-ed with the platform's per-agent monitoring mode — see [Platform monitoring mode](#platform-monitoring-mode) |
101
+ | `auto_approve_vetted_permissions` | `boolean` | `false` | Auto-resolve Claude Code's native `PermissionRequest` dialog when `PreToolUse` already vetted the same call — see [`PermissionRequest`](#4-permissionrequest--auto-resolving-the-native-dialog) below |
102
+
103
+ ### Dev fallback: running without a plugin host
104
+
105
+ The same `dist/index.js` also supports the original dev workflow of piping synthetic stdin payloads straight into the script, with no plugin host involved. In that mode, configuration comes from `claude-code.json` (gitignored, lives next to `package.json`) or `DATAGUSTO_SAFETY_*` environment variables — both still fully supported, and checked when no `CLAUDE_PLUGIN_OPTION_*` values are present:
106
+
107
+ ```json
108
+ {
109
+ "serverUrl": "https://your-datagusto-server.example.com",
110
+ "apiKey": "your-api-key",
111
+ "timeoutMs": 30000,
112
+ "failClosed": true,
113
+ "verbose": false,
114
+ "monitorOnly": false,
115
+ "autoApproveVettedPermissions": false
116
+ }
117
+ ```
118
+
119
+ | Option | Env var override |
120
+ |--------|------------------|
121
+ | `serverUrl` | `DATAGUSTO_SAFETY_SERVER_URL` |
122
+ | `apiKey` | `DATAGUSTO_SAFETY_API_KEY` |
123
+ | `timeoutMs` | `DATAGUSTO_SAFETY_TIMEOUT_MS` |
124
+ | `failClosed` | `DATAGUSTO_SAFETY_FAIL_CLOSED` |
125
+ | `verbose` | `DATAGUSTO_SAFETY_VERBOSE` |
126
+ | `monitorOnly` | `DATAGUSTO_SAFETY_MONITOR_ONLY` |
127
+ | `autoApproveVettedPermissions` | `DATAGUSTO_SAFETY_AUTO_APPROVE_VETTED_PERMISSIONS` |
128
+
129
+ Precedence, highest first: `CLAUDE_PLUGIN_OPTION_*` (set by the plugin host from `userConfig`) → `DATAGUSTO_SAFETY_*` env vars → `claude-code.json`. Booleans accept `true`/`1`/`yes` or `false`/`0`/`no` (case-insensitive). The `claude-code.json` path can be overridden with `DATAGUSTO_SAFETY_CONFIG_PATH`.
130
+
131
+ ### Manual hook wiring (legacy, optional)
132
+
133
+ If you'd rather not load this as a plugin, the compiled script can still be wired by hand into `.claude/settings.json`'s `hooks` block — copy the `hooks` object from [`hooks/hooks.json`](hooks/hooks.json), replacing `${CLAUDE_PLUGIN_ROOT}` with the absolute path to this directory. Configuration then falls back to `claude-code.json` / `DATAGUSTO_SAFETY_*`, since there's no plugin host to supply `userConfig` values in this mode.
134
+
135
+ ## How It Works
136
+
137
+ The plugin handles seven Claude Code hook events. Each invocation is a separate process: it reads one JSON payload from stdin, does its work, writes a decision to stdout, and exits.
138
+
139
+ ### 1. `SessionStart` — Eager Tool Registration
140
+
141
+ Fires once per session (new, resumed, or post-compact). Sends the bundled built-in tool catalog (see [Built-in tool catalog](#built-in-tool-catalog)) to `POST /api/v1/public/safety/tools/register`, hash-gated so it's a no-op once already registered. Decouples registration from the first user prompt — a session that only ever runs slash commands still gets its tools registered.
142
+
143
+ ### 2. `UserPromptSubmit` — Alignment (+ Registration Fallback)
144
+
145
+ Fires once per user turn, before the prompt reaches the model:
146
+
147
+ - **Alignment**: Sends `payload.prompt` — the raw user instruction, no parsing needed — to `POST /api/v1/public/safety/sessions/alignment`. The returned `session_id` is persisted for subsequent validation calls. Prompts that are slash commands (`/compact`) or bash-mode shortcuts (`!ls`) are skipped.
148
+ - **Tool registration**: Repeats the same hash-gated registration `SessionStart` already does, as a cheap defense-in-depth fallback.
149
+
150
+ ### 3. `PreToolUse` — Pre-execution Validation
151
+
152
+ Before each tool call, calls `POST /api/v1/public/safety/sessions/validate` with `timing: "on_start"`. The response is routed by `should_proceed` and `severity`, and the verdict is cached for `PermissionRequest` (below) to consult:
153
+
154
+ | Verdict | Claude Code response | Behavior |
155
+ |---------|----------------------|----------|
156
+ | `should_proceed: false` (severity high) | `hookSpecificOutput.permissionDecision: "deny"` | Tool execution is blocked outright. |
157
+ | `severity: "medium"` + `injected_instruction` | `hookSpecificOutput.permissionDecision: "ask"` | Claude Code's normal permission prompt is shown with the injected instruction as the reason. |
158
+ | `should_proceed: true` (severity low) | _(no output)_ | Execution proceeds; Claude Code's own permission system (if any) still applies — see `PermissionRequest`. |
159
+
160
+ ### 4. `PermissionRequest` — Auto-Resolving the Native Dialog
161
+
162
+ Fires after `PreToolUse`, only when Claude Code's own permission settings would still show a dialog (i.e. `PreToolUse` passed the call through with no explicit decision). When `auto_approve_vetted_permissions` is enabled, this reuses the cached `PreToolUse` verdict to answer the dialog automatically — without a second Datagusto round-trip — instead of prompting the user again for a call Datagusto already vetted. It never touches a cached `ask` verdict (severity medium): that case exists specifically so the user sees the dialog with Datagusto's injected context. Off by default; a no-op when disabled.
163
+
164
+ ### 5. `PostToolUse` / `PostToolUseFailure` — Post-execution Validation
165
+
166
+ After each tool call succeeds (`PostToolUse`) or fails (`PostToolUseFailure` — same payload shape, same handling), calls the validate API with `timing: "on_end"` and the tool's output:
167
+
168
+ | Verdict | Claude Code response |
169
+ |---------|-----------------------|
170
+ | `should_proceed: false` | `{ "decision": "block", "reason": ... }` **and** `hookSpecificOutput.updatedToolOutput` — the tool's recorded output is replaced with a `[BLOCKED]` notice, not just accompanied by feedback. |
171
+ | `injected_instruction` (without block) | `hookSpecificOutput.additionalContext` — appended as extra context for Claude. |
172
+ | Otherwise | _(no output)_ |
173
+
174
+ ### 6. `SessionEnd` — State Cleanup
175
+
176
+ Fires once per session, on exit. Deletes the per-session state file so `os.tmpdir()/datagusto-safety-plugin/` doesn't accumulate stale entries indefinitely.
177
+
178
+ ### Monitor-only mode
179
+
180
+ Set `monitor_only: true` (or `DATAGUSTO_SAFETY_MONITOR_ONLY=true`) to log all deny/ask/block/inject/auto-approve decisions without enforcing them — useful for staging rollouts. `fail_closed` error-handling is intentionally unaffected.
181
+
182
+ ### Platform monitoring mode
183
+
184
+ Independently of `monitor_only`, the Datagusto platform has a per-agent **monitoring mode** (dashboard: agent settings → monitoring). When it is on, the server still evaluates guardrails, records every verdict and detects incidents, but returns `monitoring_mode_enabled: true` on `/sessions/alignment` and `/sessions/validate` responses and expects the client not to enforce them.
185
+
186
+ The plugin honours that flag: effective monitoring = `monitor_only` **OR** `monitoring_mode_enabled`. Either switch disables enforcement; there is no way for one to override the other. The platform value is resolved per request:
187
+
188
+ 1. `monitoring_mode_enabled` on the current validate response (follows a settings change mid-session), then
189
+ 2. the value most recently reported by any alignment or validate response for this session (kept in the per-session state file, and the only source for `PermissionRequest` and for calls where only `/guardrails/evaluate` ran, since that endpoint carries no flag), then
190
+ 3. `false` — servers that omit the field behave exactly as before.
191
+
192
+ The first time a session sees the platform flag as `true`, one user-facing notice is emitted via the hook output's `systemMessage` field regardless of `verbose` (merged into the deny / ask / block JSON when there is one, or written as a bare `{ "systemMessage": ... }` on allow paths); every skipped verdict is then logged as `[MONITOR] ... skipped (platform monitoring mode)` (vs. `(plugin monitorOnly)`) when `verbose` is on. Fail-closed handling of API errors is governed by `monitor_only` alone; the platform flag never relaxes it, because an errored request carries no verdict.
193
+
194
+ ## Fail-Closed vs Fail-Open
195
+
196
+ - **`fail_closed: true`** (default): Tool execution is denied when the Datagusto API is unreachable or errors. Safe default for production.
197
+ - **`fail_closed: false`**: Tool execution is allowed to proceed on API errors. Use for development/testing.
198
+
199
+ Misconfiguration (missing `server_url`/`api_key`) is always fail-**open** regardless of `fail_closed`. Blocking every tool call would leave the user unable to use the agent to troubleshoot. Instead, `SessionStart` shows a warning (via `systemMessage`) that tool calls aren't being checked and suggests `dg setup claude-code`, and every other hook writes the same warning to stderr and exits non-blocking. `fail_closed` still governs API errors once the plugin is configured.
200
+
201
+ ## Troubleshooting
202
+
203
+ ### Enable verbose mode
204
+
205
+ Set `verbose: true` (via `userConfig` or `claude-code.json`) to see per-hook logs on stderr, including API call results and hash-gate decisions.
206
+
207
+ ### Common errors
208
+
209
+ | Error | Cause | Solution |
210
+ |-------|-------|----------|
211
+ | `Datagusto safety plugin isn't configured` (tool calls not checked) | Missing `serverUrl` / `apiKey` | Run `dg setup claude-code`, or check the plugin's `userConfig` values, or `claude-code.json` / `DATAGUSTO_SAFETY_*` in dev-fallback mode |
212
+ | `no session_id available` (on every tool call) | `UserPromptSubmit` hook isn't firing, or alignment failed | Confirm the plugin loaded (all 7 hooks should appear in the SDK's `init` message); check `verbose` logs for the alignment API error |
213
+ | `validation API error` | Datagusto server returned non-2xx | Check server health and API key validity |
214
+ | Tool denied unexpectedly | API error with `fail_closed: true` | Set `fail_closed: false` to allow tools during API outages |
215
+
216
+ ## Built-in tool catalog
217
+
218
+ Like OpenClaw, Claude Code's hook payloads don't expose the active tool catalog (`PreToolUse`/`PostToolUse` only carry the one tool being called). To register tools with Datagusto deterministically, the plugin bundles a snapshot of Claude Code's standard built-in tools (Read, Write, Edit, NotebookEdit, Glob, Grep, Bash, BashOutput, KillShell, WebFetch, WebSearch, Task, TodoWrite, ExitPlanMode, SlashCommand) at [`src/builtin-tools.generated.ts`](src/builtin-tools.generated.ts).
219
+
220
+ Unlike the OpenClaw plugin's `scripts/generate-builtin-tools.mjs`, there is no generator script here: Claude Code's tool catalog isn't an importable module from an upstream source tree, so this file is hand-curated from [Anthropic's public docs](https://code.claude.com/docs/en/settings) instead of extracted programmatically. MCP-provided tools (`mcp__*`) are dynamic per-installation and intentionally excluded. Review this file by hand when Claude Code's built-in tool list or schemas change.
221
+
222
+ Output schemas are not exposed by Claude Code either, so they remain as the empty-object placeholder, same as OpenClaw.
223
+
224
+ ## Architecture
225
+
226
+ ```
227
+ Claude Code plugin (.claude-plugin/plugin.json + hooks/hooks.json), per hook event, one process each
228
+ |
229
+ |-- SessionStart -------> buildBuiltinToolDefinitions() --> POST /tools/register (hash-gated)
230
+ |
231
+ |-- UserPromptSubmit ---> payload.prompt --------------> POST /sessions/alignment
232
+ | -> persist session_id to state file
233
+ | -> registration fallback (hash-gated, same as SessionStart)
234
+ |
235
+ |-- PreToolUse --------> POST /sessions/validate (on_start)
236
+ | should_proceed=false --> permissionDecision: deny
237
+ | severity=medium + injected_instruction --> permissionDecision: ask
238
+ | otherwise --> (allow)
239
+ | -> cache verdict for PermissionRequest
240
+ |
241
+ |-- PermissionRequest -> (if auto_approve_vetted_permissions) replay cached verdict
242
+ | cached ask (severity=medium) --> (no-op, never auto-resolved)
243
+ | otherwise --> decision.behavior: allow/deny
244
+ |
245
+ |-- PostToolUse / -----> POST /sessions/validate (on_end)
246
+ | PostToolUseFailure should_proceed=false --> decision: block + updatedToolOutput (rewrite)
247
+ | injected_instruction --> additionalContext (feedback only)
248
+ | otherwise --> (no output)
249
+ |
250
+ |-- SessionEnd ---------> delete per-session state file
251
+ ```
252
+
253
+ State (`sessionId`, `registeredToolsHash`, `lastUserInstructionHash`, `lastPreToolUseCheck`) is a small JSON file per Claude Code `session_id` under `os.tmpdir()/datagusto-safety-plugin/`, since each hook invocation is a fresh process with no shared memory. `SessionEnd` deletes it; with no `SessionEnd` (e.g. an abnormal exit), it's simply orphaned in the OS tmpdir like any other stale temp file.
254
+
255
+ ## Differences from the OpenClaw plugin
256
+
257
+ Both plugins implement the same business semantics against the same Datagusto contract (`/sessions/validate`'s `should_proceed`/`severity`/`injected_instruction` fields), mapped independently onto each host's native hook vocabulary:
258
+
259
+ - **7 Claude Code hooks vs. OpenClaw's 5.** No Claude Code analogue of `before_agent_start` (state is namespaced by Claude Code's stable `session_id`, so there's nothing to reset) or `tool_result_persist` (folded into `PostToolUse`'s own `updatedToolOutput`, see below). Conversely, Claude Code's `SessionStart`/`SessionEnd`/`PostToolUseFailure`/`PermissionRequest` have no OpenClaw equivalent — they map onto lifecycle concepts (explicit session boundaries, a host-native permission dialog) that don't exist in OpenClaw's single-long-running-process architecture.
260
+ - **No race conditions.** OpenClaw's `before_tool_call` awaits a pending alignment promise because hooks share one process; Claude Code hooks run as separate, sequential processes, so `UserPromptSubmit`/`SessionStart` always finish before the next `PreToolUse`.
261
+ - **`PostToolUse` can rewrite output.** `hookSpecificOutput.updatedToolOutput` replaces the tool's recorded result, the same underlying capability as OpenClaw's `tool_result_persist` rewrite, expressed through Claude Code's own hook field instead.
262
+ - **Simpler instruction extraction.** `payload.prompt` is the raw current instruction; no scanning `historyMessages` and no channel-metadata stripping (only slash-command / bash-mode filtering).
263
+ - **No SDK dependency.** OpenClaw imports `OpenClawPluginApi` from the `openclaw` npm package; this plugin only reads JSON from stdin, so there's no peer dependency on Claude Code at all.
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,82 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Datagusto Safety Plugin - HTTP API Client
3
+ // ---------------------------------------------------------------------------
4
+ const TOOLS_REGISTER_PATH = "/api/v1/public/safety/tools/register";
5
+ const SESSIONS_ALIGNMENT_PATH = "/api/v1/public/safety/sessions/alignment";
6
+ const SESSIONS_VALIDATE_PATH = "/api/v1/public/safety/sessions/validate";
7
+ const GUARDRAILS_EVALUATE_PATH = "/api/v1/public/guardrails/evaluate";
8
+ export class DatagustoApiClient {
9
+ baseUrl;
10
+ apiKey;
11
+ timeoutMs;
12
+ constructor(baseUrl, apiKey, timeoutMs = 30_000) {
13
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
14
+ this.apiKey = apiKey;
15
+ this.timeoutMs = timeoutMs;
16
+ }
17
+ // -- Public API methods ---------------------------------------------------
18
+ async registerTools(tools) {
19
+ const body = { tools };
20
+ return this._post(TOOLS_REGISTER_PATH, body);
21
+ }
22
+ async postAlignment(userInstruction, sessionId, agentType) {
23
+ const body = { user_instruction: userInstruction };
24
+ if (sessionId) {
25
+ body.session_id = sessionId;
26
+ }
27
+ if (agentType) {
28
+ body.agent_type = agentType;
29
+ }
30
+ return this._post(SESSIONS_ALIGNMENT_PATH, body);
31
+ }
32
+ async postValidate(payload) {
33
+ return this._post(SESSIONS_VALIDATE_PATH, payload);
34
+ }
35
+ /**
36
+ * Evaluate the agent's assigned (dashboard-authored) guardrail rules for a
37
+ * single tool call. Agent-scoped via the API key — needs no session_id — and
38
+ * is the only path that evaluates assigned rules and on_end timing. Called
39
+ * alongside postValidate and merged (block-wins) by the hook handlers.
40
+ */
41
+ async evaluateGuardrails(payload) {
42
+ return this._post(GUARDRAILS_EVALUATE_PATH, payload);
43
+ }
44
+ // -- Internal helper ------------------------------------------------------
45
+ async _post(endpoint, body) {
46
+ const url = this.baseUrl + endpoint;
47
+ try {
48
+ const response = await fetch(url, {
49
+ method: "POST",
50
+ headers: {
51
+ Authorization: `Bearer ${this.apiKey}`,
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify(body),
55
+ signal: AbortSignal.timeout(this.timeoutMs),
56
+ });
57
+ const text = await response.text();
58
+ let data = null;
59
+ try {
60
+ data = text ? JSON.parse(text) : null;
61
+ }
62
+ catch {
63
+ // Non-JSON response — leave data as null
64
+ }
65
+ return {
66
+ ok: response.ok,
67
+ status: response.status,
68
+ data,
69
+ ...(response.ok ? {} : { error: text }),
70
+ };
71
+ }
72
+ catch (err) {
73
+ return {
74
+ ok: false,
75
+ status: null,
76
+ data: null,
77
+ error: err instanceof Error ? err.message : String(err),
78
+ };
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1,33 @@
1
+ // MANUALLY CURATED — DO NOT TREAT AS AUTO-GENERATED
2
+ // Source: Claude Code's public tool/permissions reference
3
+ // https://code.claude.com/docs/en/settings
4
+ // https://code.claude.com/docs/en/hooks
5
+ // Curated at: 2026-06-21
6
+ // Tool count: 15
7
+ //
8
+ // Unlike OpenClaw, Claude Code's built-in tool catalog is not an importable
9
+ // module — there is no upstream source tree to run a generator script
10
+ // against (see openclaw/scripts/generate-builtin-tools.mjs for that
11
+ // approach). This file is hand-maintained from Anthropic's published docs
12
+ // and product knowledge of Claude Code's standard tool set instead. MCP
13
+ // tools (`mcp__*`) are dynamic per-installation and intentionally excluded.
14
+ // Re-review this file whenever Claude Code's built-in tool list or its
15
+ // parameter schemas change.
16
+ export const BUILTIN_TOOLS = [
17
+ { "id": "Read", "description": "Read file contents", "sectionId": "fs", "sectionLabel": "Files", "inputSchema": { "type": "object", "required": ["file_path"], "properties": { "file_path": { "type": "string", "description": "Absolute path to the file to read" }, "offset": { "type": "number", "description": "Line number to start reading from" }, "limit": { "type": "number", "description": "Maximum number of lines to read" } } }, "inputSchemaSource": "manual" },
18
+ { "id": "Write", "description": "Create or overwrite a file", "sectionId": "fs", "sectionLabel": "Files", "inputSchema": { "type": "object", "required": ["file_path", "content"], "properties": { "file_path": { "type": "string", "description": "Absolute path to the file to write" }, "content": { "type": "string", "description": "Content to write to the file" } } }, "inputSchemaSource": "manual" },
19
+ { "id": "Edit", "description": "Make a targeted string replacement in a file", "sectionId": "fs", "sectionLabel": "Files", "inputSchema": { "type": "object", "required": ["file_path", "old_string", "new_string"], "properties": { "file_path": { "type": "string", "description": "Absolute path to the file to edit" }, "old_string": { "type": "string", "description": "Exact text to replace" }, "new_string": { "type": "string", "description": "Replacement text" }, "replace_all": { "type": "boolean", "description": "Replace all occurrences of old_string instead of requiring a unique match" } } }, "inputSchemaSource": "manual" },
20
+ { "id": "NotebookEdit", "description": "Edit a Jupyter notebook cell", "sectionId": "fs", "sectionLabel": "Files", "inputSchema": { "type": "object", "required": ["notebook_path", "new_source"], "properties": { "notebook_path": { "type": "string", "description": "Absolute path to the .ipynb file" }, "cell_id": { "type": "string", "description": "Target cell id" }, "new_source": { "type": "string", "description": "New cell source" }, "cell_type": { "type": "string", "enum": ["code", "markdown"] }, "edit_mode": { "type": "string", "enum": ["replace", "insert", "delete"] } } }, "inputSchemaSource": "manual" },
21
+ { "id": "Glob", "description": "Find files by glob pattern", "sectionId": "search", "sectionLabel": "Search", "inputSchema": { "type": "object", "required": ["pattern"], "properties": { "pattern": { "type": "string", "description": "Glob pattern, e.g. \"src/**/*.ts\"" }, "path": { "type": "string", "description": "Directory to search in (defaults to cwd)" } } }, "inputSchemaSource": "manual" },
22
+ { "id": "Grep", "description": "Search file contents with a regex", "sectionId": "search", "sectionLabel": "Search", "inputSchema": { "type": "object", "required": ["pattern"], "properties": { "pattern": { "type": "string", "description": "Regular expression to search for" }, "path": { "type": "string", "description": "File or directory to search" }, "glob": { "type": "string", "description": "Glob filter for files to search, e.g. \"*.ts\"" }, "type": { "type": "string", "description": "File type filter, e.g. \"js\"" }, "output_mode": { "type": "string", "enum": ["content", "files_with_matches", "count"] }, "-B": { "type": "number", "description": "Lines of context before each match" }, "-A": { "type": "number", "description": "Lines of context after each match" }, "-C": { "type": "number", "description": "Lines of context around each match" }, "-n": { "type": "boolean", "description": "Show line numbers" }, "-i": { "type": "boolean", "description": "Case-insensitive search" }, "multiline": { "type": "boolean", "description": "Allow patterns to span multiple lines" }, "head_limit": { "type": "number", "description": "Limit output to the first N matches/lines" } } }, "inputSchemaSource": "manual" },
23
+ { "id": "Bash", "description": "Execute a shell command", "sectionId": "runtime", "sectionLabel": "Shell", "inputSchema": { "type": "object", "required": ["command"], "properties": { "command": { "type": "string", "description": "Shell command to execute" }, "description": { "type": "string", "description": "Short human-readable description of the command" }, "timeout": { "type": "number", "description": "Timeout in milliseconds" }, "run_in_background": { "type": "boolean", "description": "Run the command in the background and return immediately" } } }, "inputSchemaSource": "manual" },
24
+ { "id": "BashOutput", "description": "Read output from a running/background shell", "sectionId": "runtime", "sectionLabel": "Shell", "inputSchema": { "type": "object", "required": ["bash_id"], "properties": { "bash_id": { "type": "string", "description": "Id of the background shell" }, "filter": { "type": "string", "description": "Regex to filter the returned output lines" } } }, "inputSchemaSource": "manual" },
25
+ { "id": "KillShell", "description": "Terminate a running background shell", "sectionId": "runtime", "sectionLabel": "Shell", "inputSchema": { "type": "object", "required": ["shell_id"], "properties": { "shell_id": { "type": "string", "description": "Id of the background shell to kill" } } }, "inputSchemaSource": "manual" },
26
+ { "id": "WebFetch", "description": "Fetch a URL and summarize its content", "sectionId": "web", "sectionLabel": "Web", "inputSchema": { "type": "object", "required": ["url", "prompt"], "properties": { "url": { "type": "string", "description": "URL to fetch" }, "prompt": { "type": "string", "description": "What to extract from the fetched content" } } }, "inputSchemaSource": "manual" },
27
+ { "id": "WebSearch", "description": "Search the web", "sectionId": "web", "sectionLabel": "Web", "inputSchema": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string", "description": "Search query" }, "allowed_domains": { "type": "array", "items": { "type": "string" }, "description": "Only include results from these domains" }, "blocked_domains": { "type": "array", "items": { "type": "string" }, "description": "Exclude results from these domains" } } }, "inputSchemaSource": "manual" },
28
+ { "id": "Task", "description": "Launch a sub-agent to handle a delegated task", "sectionId": "agents", "sectionLabel": "Agents", "inputSchema": { "type": "object", "required": ["description", "prompt", "subagent_type"], "properties": { "description": { "type": "string", "description": "Short (3-5 word) description of the task" }, "prompt": { "type": "string", "description": "The task for the sub-agent to perform" }, "subagent_type": { "type": "string", "description": "Which agent type to use" } } }, "inputSchemaSource": "manual" },
29
+ { "id": "TodoWrite", "description": "Create/update the session's structured task list", "sectionId": "agents", "sectionLabel": "Agents", "inputSchema": { "type": "object", "required": ["todos"], "properties": { "todos": { "type": "array", "items": { "type": "object", "required": ["content", "status", "activeForm"], "properties": { "content": { "type": "string", "description": "Imperative task description" }, "status": { "type": "string", "enum": ["pending", "in_progress", "completed"] }, "activeForm": { "type": "string", "description": "Present-continuous form shown while in progress" } } }, "description": "The updated todo list" } } }, "inputSchemaSource": "manual" },
30
+ { "id": "ExitPlanMode", "description": "Exit plan mode and present a plan for approval", "sectionId": "agents", "sectionLabel": "Agents", "inputSchema": { "type": "object", "required": ["plan"], "properties": { "plan": { "type": "string", "description": "The plan to present to the user, in markdown" } } }, "inputSchemaSource": "manual" },
31
+ { "id": "SlashCommand", "description": "Execute a custom slash command", "sectionId": "agents", "sectionLabel": "Agents", "inputSchema": { "type": "object", "required": ["command"], "properties": { "command": { "type": "string", "description": "The slash command to execute, including any arguments" } } }, "inputSchemaSource": "manual" },
32
+ ];
33
+ //# sourceMappingURL=builtin-tools.generated.js.map