agent-blocked 0.1.4 → 0.1.7

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 CHANGED
@@ -16,7 +16,9 @@ Report a blocker manually:
16
16
  npx agent-blocked@latest report --event=needs_direction --severity=medium --reason="Need human direction"
17
17
  ```
18
18
 
19
- The installer adds a small `.agent-blocked/` directory plus tool-specific config or instruction files for the selected agent tools. Those files tell the agent when to escalate during goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode. They also provide a local reporter command that sends blocker events to your Agent Blocked alert profile.
19
+ The installer adds a small `.agent-blocked/` directory plus tool-specific config, hooks, or instruction files for the selected agent tools. Those files tell the agent when to escalate during goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode. They also provide a local reporter command that sends blocker events to your Agent Blocked alert profile.
20
+
21
+ For tools with native hooks, such as Claude Code and Codex, installing the adapter turns the hook on for that project. Make sure `AGENT_BLOCKED_AGENT_TOKEN` is available in the shell or secret store used by the agent.
20
22
 
21
23
  Agents should not notify during normal interactive paired work when the human is already present and responding in the current conversation. In automated modes, agents should notify when continuing would waste time, create risk, or require a decision only the human can make:
22
24
 
@@ -27,12 +29,10 @@ Agents should not notify during normal interactive paired work when the human is
27
29
  - `low_confidence` when the agent can continue but the chance of being wrong is high.
28
30
  - `hard_blocked` when no productive path remains without human input.
29
31
 
30
- Set these environment variables in the shell where the agent runs:
32
+ Set the scoped token in the shell or secret store where the agent runs:
31
33
 
32
34
  ```bash
33
- export AGENT_BLOCKED_WEBHOOK_URL="https://agentblocked.com/api/agent-blocked"
34
35
  export AGENT_BLOCKED_AGENT_TOKEN="your-scoped-token"
35
- export AGENT_NAME="prod-agent"
36
36
  ```
37
37
 
38
38
  Agents can report through the installed local reporter:
@@ -41,7 +41,9 @@ Agents can report through the installed local reporter:
41
41
  node .agent-blocked/report.mjs --event=needs_credentials --severity=critical --reason="Missing deploy credentials"
42
42
  ```
43
43
 
44
- Codex can also be run through the wrapper so hard stops are reported automatically:
44
+ Codex hook reporting is installed by `npx agent-blocked@latest install --tool=codex`. Restart Codex or start a new Codex session after installation so the hook config is loaded.
45
+
46
+ Codex can also be run through the wrapper when you want hard process exits reported:
45
47
 
46
48
  ```bash
47
49
  npx agent-blocked@latest codex -- codex "implement the requested change"
@@ -154,6 +154,119 @@ try {
154
154
  }
155
155
  `;
156
156
 
157
+ const codexHookScript = `#!/usr/bin/env node
158
+
159
+ let input = "";
160
+ process.stdin.setEncoding("utf8");
161
+ for await (const chunk of process.stdin) input += chunk;
162
+
163
+ let event = {};
164
+ try {
165
+ event = input ? JSON.parse(input) : {};
166
+ } catch {
167
+ event = {};
168
+ }
169
+
170
+ const webhookUrl = process.env.AGENT_BLOCKED_WEBHOOK_URL || "https://agentblocked.com/api/agent-blocked";
171
+ const token = process.env.AGENT_BLOCKED_AGENT_TOKEN;
172
+ const agentId = process.env.AGENT_NAME || "codex";
173
+
174
+ if (!token) process.exit(0);
175
+
176
+ function pick(...values) {
177
+ return values.find((value) => value !== undefined && value !== null && value !== "");
178
+ }
179
+
180
+ function hookEventName(payload) {
181
+ return pick(payload.hook_event_name, payload.hookEventName, payload.event_name, payload.eventName, payload.event, payload.type, "");
182
+ }
183
+
184
+ function toolName(payload) {
185
+ return pick(payload.tool_name, payload.toolName, payload.tool, payload.tool_call?.name, payload.toolCall?.name, "");
186
+ }
187
+
188
+ function exitCode(payload) {
189
+ return pick(
190
+ payload.exit_code,
191
+ payload.exitCode,
192
+ payload.result?.exit_code,
193
+ payload.result?.exitCode,
194
+ payload.tool_result?.exit_code,
195
+ payload.toolResult?.exitCode
196
+ );
197
+ }
198
+
199
+ function failed(payload) {
200
+ if (payload.success === false || payload.ok === false) return true;
201
+ if (payload.error || payload.error_message || payload.errorMessage) return true;
202
+ const code = exitCode(payload);
203
+ return code !== undefined && code !== null && Number(code) !== 0;
204
+ }
205
+
206
+ function commandText(payload) {
207
+ return pick(
208
+ payload.command,
209
+ payload.cmd,
210
+ payload.tool_input?.cmd,
211
+ payload.toolInput?.cmd,
212
+ payload.tool_input?.command,
213
+ payload.toolInput?.command,
214
+ ""
215
+ );
216
+ }
217
+
218
+ const eventName = hookEventName(event);
219
+ const tool = toolName(event);
220
+ let payload = null;
221
+
222
+ if (eventName === "PermissionRequest" || /permission/i.test(eventName)) {
223
+ payload = {
224
+ eventType: "approval_required",
225
+ severity: "medium",
226
+ reason: \`Codex requested approval\${tool ? \` for \${tool}\` : ""}.\`
227
+ };
228
+ } else if ((eventName === "PostToolUse" || /post.*tool/i.test(eventName)) && failed(event)) {
229
+ const code = exitCode(event);
230
+ payload = {
231
+ eventType: "tool_failure",
232
+ severity: "medium",
233
+ reason: \`Codex tool failed\${tool ? \` during \${tool}\` : ""}\${code !== undefined ? \` with exit code \${code}\` : ""}.\`
234
+ };
235
+ }
236
+
237
+ if (!payload) process.exit(0);
238
+
239
+ const details = {
240
+ hookEventName: eventName || "unknown",
241
+ toolName: tool || undefined,
242
+ command: commandText(event) || undefined,
243
+ cwd: event.cwd,
244
+ sessionId: event.session_id || event.sessionId,
245
+ threadId: event.thread_id || event.threadId,
246
+ exitCode: exitCode(event)
247
+ };
248
+
249
+ try {
250
+ await fetch(webhookUrl, {
251
+ method: "POST",
252
+ headers: {
253
+ authorization: \`Bearer \${token}\`,
254
+ "content-type": "application/json"
255
+ },
256
+ body: JSON.stringify({
257
+ agentId,
258
+ ...payload,
259
+ details: JSON.stringify(details),
260
+ provider: "openai",
261
+ model: "codex",
262
+ runId: details.sessionId || details.threadId
263
+ })
264
+ });
265
+ } catch {
266
+ // Hooks should not break Codex when notification delivery fails.
267
+ }
268
+ `;
269
+
157
270
  const instructions = `# Agent Blocked instructions for coding agents
158
271
 
159
272
  Use Agent Blocked only when this agent is running without an actively engaged human in the loop, such as goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode.
@@ -173,11 +286,17 @@ node .agent-blocked/report.mjs --event=needs_direction --severity=medium --reaso
173
286
  Environment expected by the report command:
174
287
 
175
288
  \`\`\`bash
176
- export AGENT_BLOCKED_WEBHOOK_URL="https://agentblocked.com/api/agent-blocked"
177
289
  export AGENT_BLOCKED_AGENT_TOKEN="your-scoped-token"
178
- export AGENT_NAME="your-agent-name"
179
290
  \`\`\`
180
291
 
292
+ For Codex hook-based automatic reporting, install the Codex adapter and restart Codex or start a new Codex session so the hook config is loaded:
293
+
294
+ \`\`\`bash
295
+ npx agent-blocked@latest install --tool=codex
296
+ \`\`\`
297
+
298
+ The hook only reports matching hook events such as approval requests and failed tool calls.
299
+
181
300
  Event types:
182
301
 
183
302
  - hard_blocked: no productive path remains without human input.
@@ -252,6 +371,37 @@ function mergeClaudeSettings(path, settings) {
252
371
  console.log(`Updated ${path} with Agent Blocked hooks.`);
253
372
  }
254
373
 
374
+ function mergeCodexHooks(path, config) {
375
+ let currentConfig = {};
376
+ let currentHooks = {};
377
+
378
+ if (existsSync(path)) {
379
+ const existingRaw = readFileSync(path, "utf8");
380
+ try {
381
+ const parsed = JSON.parse(existingRaw);
382
+ currentConfig = parsed && typeof parsed === "object" ? parsed : {};
383
+ currentHooks = currentConfig.hooks && typeof currentConfig.hooks === "object" ? currentConfig.hooks : {};
384
+ } catch {
385
+ console.log(`Could not parse ${path}. Wrote example file for manual merge instead.`);
386
+ writeFileIfMissing(join(root, ".codex", "hooks.agent-blocked.example.json"), `${JSON.stringify(config, null, 2)}\n`);
387
+ return;
388
+ }
389
+ }
390
+
391
+ const mergedHooks = { ...currentHooks };
392
+ for (const [eventName, hooks] of Object.entries(config.hooks || {})) {
393
+ mergedHooks[eventName] = mergeHookEntries(
394
+ Array.isArray(mergedHooks[eventName]) ? mergedHooks[eventName] : [],
395
+ hooks || []
396
+ );
397
+ }
398
+
399
+ currentConfig.hooks = mergedHooks;
400
+ mkdirSync(dirname(path), { recursive: true });
401
+ writeFileSync(path, `${JSON.stringify(currentConfig, null, 2)}\n`);
402
+ console.log(`Updated ${path} with Agent Blocked hooks.`);
403
+ }
404
+
255
405
  function upsertMarkedSection(path, title) {
256
406
  const section = `${markerStart}
257
407
  ## ${title}
@@ -297,6 +447,26 @@ function installClaude() {
297
447
  }
298
448
 
299
449
  function installCodex() {
450
+ writeFileIfMissing(join(root, ".agent-blocked", "codex-hook.mjs"), codexHookScript);
451
+ const hooks = {
452
+ hooks: {
453
+ PermissionRequest: [
454
+ {
455
+ matcher: "*",
456
+ hooks: [{ type: "command", command: "node \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agent-blocked/codex-hook.mjs\"" }]
457
+ }
458
+ ],
459
+ PostToolUse: [
460
+ {
461
+ matcher: "*",
462
+ hooks: [{ type: "command", command: "node \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agent-blocked/codex-hook.mjs\"" }]
463
+ }
464
+ ]
465
+ }
466
+ };
467
+
468
+ writeFileIfMissing(join(root, ".codex", "hooks.agent-blocked.example.json"), `${JSON.stringify(hooks, null, 2)}\n`);
469
+ mergeCodexHooks(join(root, ".codex", "hooks.json"), hooks);
300
470
  upsertMarkedSection(join(root, "AGENTS.md"), "Codex Agent Blocked setup");
301
471
  }
302
472
 
@@ -329,7 +499,10 @@ function install() {
329
499
  installers[name]();
330
500
  }
331
501
 
332
- console.log("\nNext: set AGENT_BLOCKED_WEBHOOK_URL, AGENT_BLOCKED_AGENT_TOKEN, and AGENT_NAME in the shell where your agent runs.");
502
+ console.log("\nNext: set AGENT_BLOCKED_AGENT_TOKEN in the shell or secret store where your agent runs.");
503
+ if (selected.includes("codex")) {
504
+ console.log("Codex hook reporting is installed. Restart Codex or start a new Codex session if it was already running.");
505
+ }
333
506
  console.log("Manual report command: node .agent-blocked/report.mjs --event=needs_direction --severity=medium --reason=\"Need human direction\"");
334
507
  }
335
508
 
@@ -376,7 +549,11 @@ async function codex() {
376
549
  const command = args[0] || "codex";
377
550
  const commandArgs = args[0] ? args.slice(1) : [];
378
551
 
379
- const child = spawn(command, commandArgs, { stdio: "inherit", shell: false });
552
+ const child = spawn(command, commandArgs, {
553
+ stdio: "inherit",
554
+ shell: false,
555
+ env: process.env
556
+ });
380
557
  const code = await new Promise((resolve) => child.on("close", resolve));
381
558
 
382
559
  if (code && process.env.AGENT_BLOCKED_AGENT_TOKEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-blocked",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "CLI installer and reporter for Agent Blocked AI agent escalation alerts.",
5
5
  "type": "module",
6
6
  "bin": {