SLOPSHOPPER

auto-mode

Enhancements for Claude Code auto mode

newguard
★ 17v?MITupdated 2026-10-08bendrucker/claude/plugins/auto-mode
A shopper browsing a rack in a slop shop
README

auto-mode

Enhancements for Claude Code auto mode.

Mod

Records each tool call's permission verdict. A function-hooks mod, so it loads only where CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. It depends on the mod-events plugin, which writes its events.

  • tool.check: keeps the engine's verdict (allow, ask, deny), the deciding rule or hook, and the loop's agentId.
  • tool.call: times the call and emits a tool.verdict event with the verdict. Tools that wait on the person (AskUserQuestion, the plan-mode tools) are marked interactive.
  • session.start: emits a session.start event.
  • turn.step: emits a server.tool event for each tool the API ran inside a request.

The observability:session index ingests the events as mod_events and reads them through the classifier_verdicts view. The engine does not expose the permission mode to tool.check, so the records cannot tell an auto-mode classifier ask from a dialog ask.

Testing

bun scripts/mod-test.ts auto-mode
Source 1 files
mod/register.ts 115 lines
1import type { EngineInterface, On, ToolCallResult } from "claude-code";
2
3type ModEventsInput = Parameters<EngineInterface["modEvents"]["emit"]>[0];
4
5const MOD = "auto-mode";
6
7/**
8 * Tools whose run waits on the person by design, so their wall time measures the person.
9 */
10const INTERACTIVE = new Set(["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"]);
11
12interface Verdict {
13  decision: string;
14  reason: string | null;
15  rule: string | null;
16  hook: string | null;
17  checkedAt: number;
18}
19
20function keyOf(agentId: string | undefined, toolUseId: string): string {
21  return `${agentId ?? ""}/${toolUseId}`;
22}
23
24function outcomeOf(result: ToolCallResult): "ok" | "error" | "deny" {
25  if (result.deny !== undefined) return "deny";
26  if (result.isError) return "error";
27  return "ok";
28}
29
30/**
31 * Records each tool call's permission verdict and each server tool the API ran.
32 * An `ask` goes to the mode's decider, which the mod API does not expose, so
33 * a verdict's `ms` spans the decider plus the tool. `check_ms` is the time to the verdict.
34 */
35export function register(on: On): void {
36  const verdicts = new Map<string, Verdict>();
37
38  on("session.start", ($, e, next) => {
39    void $.modEvents.emit({ mod: MOD, event: "session.start" });
40    return next(e);
41  });
42
43  // Both hooks only observe, so a failure lets the call go on as the engine settled it.
44  on("tool.check", async ($, e, next) => {
45    const verdict = await next(e);
46    if (e.tool_use_id !== undefined) {
47      verdicts.set(keyOf(e.agentId, e.tool_use_id), {
48        decision: verdict.decision,
49        reason: verdict.reason ?? null,
50        rule: verdict.rule ?? null,
51        hook: verdict.hook ?? null,
52        checkedAt: await $.clock.now(),
53      });
54    }
55    return verdict;
56  }).catch(($, e, next) => next(e));
57
58  on("tool.call", async ($, e, next) => {
59    const key = keyOf(e.agentId, e.tool_use_id);
60    const startedAt = await $.clock.now();
61    const settled: { result: ToolCallResult } | { thrown: unknown } = await next(e).then(
62      (result: ToolCallResult) => ({ result }),
63      (thrown: unknown) => ({ thrown }),
64    );
65    const verdict = verdicts.get(key);
66    verdicts.delete(key);
67    const finishedAt = await $.clock.now();
68    const outcome = "result" in settled ? outcomeOf(settled.result) : "throw";
69    void $.modEvents.emit({
70      mod: MOD,
71      event: "tool.verdict",
72      ok: outcome === "ok",
73      ms: finishedAt - startedAt,
74      detail: {
75        tool_use_id: e.tool_use_id,
76        agent_id: e.agentId ?? null,
77        tool: e.tool,
78        interactive: INTERACTIVE.has(e.tool),
79        decision: verdict?.decision ?? null,
80        rule: verdict?.rule ?? null,
81        hook: verdict?.hook ?? null,
82        reason: verdict?.reason ?? null,
83        started_at: startedAt,
84        check_ms: verdict === undefined ? null : verdict.checkedAt - startedAt,
85        outcome,
86      },
87    });
88    if ("thrown" in settled) throw settled.thrown;
89    return settled.result;
90  }).catch(($, e, next) => next(e));
91
92  on("turn.step", async function* ($, e, next) {
93    const response = yield* next(e);
94    const events: ModEventsInput[] = [];
95    for (const use of response.serverToolUses ?? []) {
96      const event: ModEventsInput = {
97        mod: MOD,
98        event: "server.tool",
99        detail: {
100          tool_use_id: use.id,
101          agent_id: e.agentId ?? null,
102          tool: use.name,
103          turn_id: response.turnId,
104          step: response.index,
105          started_at: use.startedAt,
106        },
107      };
108      if (use.endedAt !== undefined) event.ms = use.endedAt - use.startedAt;
109      events.push(event);
110    }
111    await Promise.all(events.map((event) => $.modEvents.emit(event)));
112    return response;
113  });
114}
115