auto-mode 0.4.4 → 0.5.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.
package/README.md CHANGED
@@ -6,9 +6,10 @@
6
6
  </p>
7
7
  </div>
8
8
 
9
- **auto-mode** judges coding-agent tool calls through a permission hook. It returns allow, deny, or
10
- no verdict to Claude Code, Codex, and Muse Code. Local rules settle read-only actions and build
11
- cleanup; Jev evaluates the remaining actions against a written policy.
9
+ **auto-mode** judges the tool calls that Claude Code would prompt for. It is a core library and a
10
+ Claude Code mod. The mod returns allow, deny, or no verdict before the permission dialog appears.
11
+ Local rules settle read-only actions and build cleanup; Jev evaluates the remaining actions against
12
+ a written policy.
12
13
 
13
14
  The Jev request includes the policy, your configured environment and permissions, the complete
14
15
  proposed action, and the last direct user message. It excludes the rest of the session. Configure
@@ -19,12 +20,12 @@ classifier.
19
20
 
20
21
  ```sh
21
22
  npm i -g auto-mode
22
- auto-mode init claude --event permission-request
23
+ claude --plugin-dir "$(npm root -g)/auto-mode/mods/auto-mode"
23
24
  ```
24
25
 
25
- Paste the printed hook entry into the file it identifies. `init` prints configuration and never
26
- writes into a harness's settings. [Harnesses](./docs/guides/harnesses.md) covers registration for
27
- each harness.
26
+ The package ships the mod in `mods/auto-mode`. The mod runs the `auto-mode` executable on `PATH`;
27
+ its `command` option takes another executable. [Claude permission mod](./docs/guides/claude-mod.md)
28
+ covers the mod, its time limits, and a trial session.
28
29
 
29
30
  Create the classifier configuration:
30
31
 
@@ -35,8 +36,8 @@ Create the classifier configuration:
35
36
  }
36
37
  ```
37
38
 
38
- Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key. Hooks that run with a
39
- scrubbed environment need `provider.apiKeyCommand` instead.
39
+ Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set
40
+ `provider.apiKeyCommand` to a command that prints it.
40
41
  [Configuration](./docs/guides/configuration.md) covers credentials and provider overrides.
41
42
 
42
43
  ## Standing permissions
@@ -51,7 +52,7 @@ consent. [False-positive clarification](./docs/guides/policy.md#false-positive-c
51
52
  clear Policy Tampering and Audit Tampering. The last direct user message can supply the exact action
52
53
  and target that a soft block needs; replies such as “go ahead” cannot supply an unseen proposal.
53
54
 
54
- Read [the policy](./policy/rules.md) before you enable the hook.
55
+ Read [the policy](./policy/rules.md) before you enable the mod.
55
56
  [Writing a policy](./docs/guides/policy.md) covers the precedence and the limits of an action-only
56
57
  assessment.
57
58
 
@@ -62,33 +63,47 @@ approval when every rule confidently allows the action, and manual approval for
62
63
  The confidence threshold is configurable and needs evaluation against your actions.
63
64
 
64
65
  A missing key, failed API call, malformed response, or oversized request follows `onFailure`. The
65
- default `defer` writes no verdict and leaves the harness to decide; `deny` fails closed. Failures
66
- write a diagnostic to stderr. auto-mode never truncates a Jev action to make it fit.
66
+ default `defer` writes no verdict and keeps the prompt; `deny` fails closed. Failures write a
67
+ diagnostic to stderr. auto-mode never truncates a Jev action to make it fit.
67
68
 
68
69
  ## Commands
69
70
 
70
71
  | Command | Effect |
71
72
  | ---------------------------- | ----------------------------------------- |
72
- | `auto-mode run` | Read a hook payload and write a verdict |
73
+ | `auto-mode run` | Read a mod request and write a verdict |
73
74
  | `auto-mode run --explain` | Write decision details to stderr |
74
75
  | `auto-mode run --local-only` | Skip the model tier |
75
76
  | `auto-mode run --jev-only` | Require Jev with a 5-second API timeout |
76
77
  | `auto-mode print-prompt` | Print the selected provider's base policy |
77
- | `auto-mode init <harness>` | Print a hook entry |
78
78
 
79
- Existing `spark`, `claude`, and `glm` presets use the Messages API and conversation history. Select
80
- `jev` explicitly to change an existing provider configuration.
79
+ The mod evaluates only ask decisions and preserves existing allow and deny decisions. It runs the
80
+ CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, and `glm` presets use the
81
+ Messages API and serve library callers.
81
82
 
82
- The optional [Claude permission mod](./docs/guides/claude-mod.md) evaluates ask decisions before the
83
- permission dialog. It preserves existing allow and deny decisions and has a bounded child process.
83
+ ## Library
84
+
85
+ ```ts
86
+ import { classifyAction, loadConfig, parseActionRequest } from 'auto-mode';
87
+
88
+ const request = parseActionRequest(body);
89
+
90
+ if (request !== null) {
91
+ const outcome = await classifyAction(request, await loadConfig());
92
+ }
93
+ ```
94
+
95
+ `classifyAction` runs both tiers and returns the verdict, a note, and diagnostics. A request carries
96
+ the session identity, the action, and the task context; `parseActionRequest` builds one from the
97
+ mod's JSON.
84
98
 
85
99
  ## Documentation
86
100
 
87
- - [Architecture](./docs/architecture/overview.md) covers the request, decision combination, and wire
88
- contract.
101
+ - [Architecture](./docs/architecture/overview.md) covers the request, decision combination, and the
102
+ mod contract.
89
103
  - [Configuration](./docs/guides/configuration.md) covers every setting and the imported Claude
90
104
  rules.
91
- - [Harnesses](./docs/guides/harnesses.md) covers registration and hook trust.
105
+ - [Claude permission mod](./docs/guides/claude-mod.md) covers the request, the verdict, and the time
106
+ limits.
92
107
  - [Writing a policy](./docs/guides/policy.md) covers rule edits and evaluation.
93
108
 
94
109
  ## Licence
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, i as loadClaudeRules, l as SETUP_NOTES, n as classifyWithModel, o as loadPolicy, s as EVENT_NOTES, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-BdXQYjfj.js";
2
+ import { n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-FdG6gahX.js";
3
3
  import { appendFile, mkdir } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
@@ -24,15 +24,12 @@ async function writeActionDiagnostic(payload, entry) {
24
24
  const stateDir = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
25
25
  const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(stateDir, "auto-mode", "actions.jsonl");
26
26
  if (path === "") return;
27
- const actionID = payload.raw["tool_use_id"] ?? payload.raw["prompt_id"] ?? payload.raw["turn_id"];
28
27
  const record = {
29
- schemaVersion: 1,
28
+ schemaVersion: 2,
30
29
  time: (/* @__PURE__ */ new Date()).toISOString(),
31
30
  invocationID: entry.invocationID,
32
- sessionHash: toHash(payload.sessionId),
33
- actionHash: typeof actionID === "string" ? toHash(actionID) : null,
34
- harness: payload.harness,
35
- event: payload.event,
31
+ sessionHash: toHash(payload.sessionID),
32
+ actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
36
33
  status: entry.status,
37
34
  verdict: entry.verdict ?? null,
38
35
  diagnostics: entry.diagnostics ?? null
@@ -52,17 +49,15 @@ function toHash(value) {
52
49
  }
53
50
  //#endregion
54
51
  //#region src/cli.ts
55
- const USAGE = `auto-mode — a permission classifier that runs as a hook
52
+ const USAGE = `auto-mode — a permission classifier for the auto-mode Claude Code mod
56
53
 
57
54
  Usage:
58
- auto-mode run Read a hook payload on stdin, write a verdict on stdout
55
+ auto-mode run Read an action request on stdin, write a verdict on stdout
59
56
  auto-mode print-prompt Print the system prompt the classifier receives
60
- auto-mode init <harness> Print the hook entry to add: claude, codex or muse
61
57
 
62
58
  Options:
63
59
  --classifier <path> Use this framework file instead of the shipped one
64
60
  --rules <path> Use this rule list instead of the shipped one
65
- --event <event> With init: pre-tool-use, the default, or permission-request
66
61
  --explain With run: also write the reasoning to stderr
67
62
  --local-only With run: skip the model tier
68
63
  --jev-only With run: require Jev and cap its API timeout at 5 seconds
@@ -79,10 +74,10 @@ async function run(explain, localOnly, jevOnly, deadline) {
79
74
  } catch {
80
75
  return printNote(explain, "stdin is not JSON, so no verdict");
81
76
  }
82
- const payload = parsePayload(body);
83
- if (payload === null) return printNote(explain, "not a tool gate this hook judges, so no verdict");
77
+ const request = parseActionRequest(body);
78
+ if (request === null) return printNote(explain, "not an action request this classifier judges, so no verdict");
84
79
  const invocationID = randomUUID();
85
- await writeActionDiagnostic(payload, {
80
+ await writeActionDiagnostic(request, {
86
81
  invocationID,
87
82
  status: "started"
88
83
  });
@@ -90,64 +85,30 @@ async function run(explain, localOnly, jevOnly, deadline) {
90
85
  try {
91
86
  loaded = await loadConfig();
92
87
  } catch {
93
- await writeActionDiagnostic(payload, {
88
+ await writeActionDiagnostic(request, {
94
89
  invocationID,
95
90
  status: "failure",
96
91
  verdict: "defer"
97
92
  });
98
- return printNote(true, "configuration unreadable; deferring to the harness");
93
+ return printNote(true, "configuration unreadable; no verdict");
99
94
  }
100
95
  const config = jevOnly ? buildJevOnlyConfig(loaded) : loaded;
101
96
  if (config === null) {
102
- await writeActionDiagnostic(payload, {
97
+ await writeActionDiagnostic(request, {
103
98
  invocationID,
104
99
  status: "failure",
105
100
  verdict: "defer"
106
101
  });
107
- return printNote(true, "Jev-only evaluation requires system-one; deferring to the harness");
108
- }
109
- let configured = null;
110
- if (config.provider.protocol === "system-one") try {
111
- configured = await loadClaudeRules(config.claudeSettingsPath);
112
- } catch {
113
- if (config.onFailure === "deny") writeVerdict(payload.event, {
114
- kind: "deny",
115
- rule: "Classifier Unavailable",
116
- reason: "Claude settings unreadable"
117
- });
118
- await writeActionDiagnostic(payload, {
119
- invocationID,
120
- status: "failure",
121
- verdict: config.onFailure === "deny" ? "deny" : "defer"
122
- });
123
- return printNote(true, "Claude settings unreadable; classifier unavailable");
124
- }
125
- const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(payload);
126
- if (local.kind === "allow") {
127
- writeVerdict(payload.event, { kind: "allow" });
128
- await writeActionDiagnostic(payload, {
129
- invocationID,
130
- status: "allow",
131
- verdict: "allow"
132
- });
133
- return printNote(explain, `allowed by ${local.exception} (${payload.harness}, local)`);
134
- }
135
- if (localOnly) {
136
- await writeActionDiagnostic(payload, {
137
- invocationID,
138
- status: "skipped",
139
- verdict: "defer"
140
- });
141
- return printNote(explain, `${payload.toolName} needs the model tier, which --local-only skipped`);
102
+ return printNote(true, "Jev-only evaluation requires system-one; no verdict");
142
103
  }
143
104
  const deadlineAt = deadline === void 0 ? void 0 : Number(deadline);
144
105
  if (deadlineAt !== void 0 && (!jevOnly || !Number.isSafeInteger(deadlineAt) || deadlineAt <= 0)) {
145
- await writeActionDiagnostic(payload, {
106
+ await writeActionDiagnostic(request, {
146
107
  invocationID,
147
108
  status: "failure",
148
109
  verdict: "defer"
149
110
  });
150
- return printNote(true, "invalid Jev evaluation deadline; deferring to the harness");
111
+ return printNote(true, "invalid Jev evaluation deadline; no verdict");
151
112
  }
152
113
  const controller = new AbortController();
153
114
  const stopEvaluation = () => {
@@ -158,14 +119,15 @@ async function run(explain, localOnly, jevOnly, deadline) {
158
119
  process.on("SIGINT", stopEvaluation);
159
120
  }
160
121
  try {
161
- const outcome = await classifyWithModel(payload, config, {
122
+ const outcome = await classifyAction(request, config, {
123
+ localOnly,
162
124
  deadlineAt,
163
125
  signal: controller.signal
164
126
  });
165
- if (outcome.verdict !== null) writeVerdict(payload.event, outcome.verdict);
166
- await writeActionDiagnostic(payload, {
127
+ if (outcome.verdict !== null) writeVerdict(outcome.verdict);
128
+ await writeActionDiagnostic(request, {
167
129
  invocationID,
168
- status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure",
130
+ status: outcome.status,
169
131
  verdict: outcome.verdict?.kind ?? "defer",
170
132
  ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
171
133
  });
@@ -175,18 +137,14 @@ async function run(explain, localOnly, jevOnly, deadline) {
175
137
  process.off("SIGINT", stopEvaluation);
176
138
  }
177
139
  }
178
- function writeVerdict(event, verdict) {
179
- const rendered = renderVerdict(event, verdict);
140
+ function writeVerdict(verdict) {
141
+ const rendered = renderVerdict(verdict);
180
142
  if (rendered !== null) process.stdout.write(rendered);
181
143
  }
182
144
  function printNote(explain, message) {
183
145
  if (explain) process.stderr.write(`auto-mode: ${message}\n`);
184
146
  return 0;
185
147
  }
186
- const HOOK_EVENTS = {
187
- "pre-tool-use": "PreToolUse",
188
- "permission-request": "PermissionRequest"
189
- };
190
148
  async function main(argv) {
191
149
  const args = parseArgs({
192
150
  args: [...argv],
@@ -194,7 +152,6 @@ async function main(argv) {
194
152
  options: {
195
153
  classifier: { type: "string" },
196
154
  rules: { type: "string" },
197
- event: { type: "string" },
198
155
  explain: { type: "boolean" },
199
156
  "local-only": { type: "boolean" },
200
157
  "jev-only": { type: "boolean" },
@@ -213,28 +170,7 @@ async function main(argv) {
213
170
  if (command === "run") try {
214
171
  return await run(args.values.explain === true, args.values["local-only"] === true, args.values["jev-only"] === true, args.values["evaluation-deadline"]);
215
172
  } catch {
216
- process.stderr.write("auto-mode: configuration unreadable; deferring to the harness\n");
217
- return 0;
218
- }
219
- if (command === "init") {
220
- const [, harness] = args.positionals;
221
- if (harness !== "claude" && harness !== "codex" && harness !== "muse") {
222
- process.stderr.write("auto-mode init: name a harness — claude, codex or muse\n");
223
- return 2;
224
- }
225
- const event = HOOK_EVENTS[args.values.event ?? "pre-tool-use"];
226
- if (event === void 0) {
227
- process.stderr.write("auto-mode init: --event takes pre-tool-use or permission-request\n");
228
- return 2;
229
- }
230
- if (event === "PermissionRequest" && harness !== "claude") {
231
- process.stderr.write(`auto-mode init: only Claude Code sends PermissionRequest\n`);
232
- return 2;
233
- }
234
- process.stdout.write(`# Add this to ${SETTINGS_PATHS[harness]}\n`);
235
- process.stdout.write(`# Configuration lives at ${resolveConfigPath()}\n`);
236
- process.stdout.write(`${buildHookConfig(harness, `${process.execPath} ${process.argv[1] ?? "auto-mode"} run`, event)}\n`);
237
- for (const line of [...SETUP_NOTES[harness], ...EVENT_NOTES[event]]) process.stdout.write(`# ${line}\n`);
173
+ process.stderr.write("auto-mode: configuration unreadable; no verdict\n");
238
174
  return 0;
239
175
  }
240
176
  if (command === "print-prompt") {
package/dist/index.d.ts CHANGED
@@ -19,7 +19,6 @@ interface Config {
19
19
  readonly provider: ProviderConfig;
20
20
  readonly classifierPath?: string | undefined;
21
21
  readonly rulesPath?: string | undefined;
22
- readonly transcriptEntries: number;
23
22
  readonly onFailure: 'defer' | 'deny';
24
23
  readonly claudeSettingsPath?: string | null | undefined;
25
24
  readonly minConfidence?: number | undefined;
@@ -30,9 +29,7 @@ export declare function resolveConfigPath(): string;
30
29
  export declare function loadConfig(path?: string): Promise<Config>;
31
30
  export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
32
31
  //#endregion
33
- //#region src/harness/types.d.ts
34
- type Harness = 'claude' | 'codex' | 'muse';
35
- type HookEvent = 'PreToolUse' | 'PermissionRequest';
32
+ //#region src/request/types.d.ts
36
33
  interface UserTask {
37
34
  readonly text: string;
38
35
  readonly origin: 'composer' | 'bridge' | 'sdk';
@@ -50,16 +47,13 @@ interface DecisionContext {
50
47
  readonly reason: 'unavailable' | 'budget';
51
48
  }[];
52
49
  }
53
- interface HookPayload {
54
- readonly harness: Harness;
55
- readonly event: HookEvent;
56
- readonly sessionId: string;
50
+ interface ActionRequest {
51
+ readonly sessionID: string;
52
+ readonly toolUseID?: string | undefined;
57
53
  readonly cwd: string;
58
54
  readonly toolName: string;
59
55
  readonly toolInput: Readonly<Record<string, unknown>>;
60
- readonly transcriptPath?: string | undefined;
61
56
  readonly decisionContext?: DecisionContext | undefined;
62
- readonly raw: Readonly<Record<string, unknown>>;
63
57
  }
64
58
  type Verdict = {
65
59
  readonly kind: 'allow';
@@ -71,19 +65,6 @@ type Verdict = {
71
65
  readonly reason: string;
72
66
  };
73
67
  //#endregion
74
- //#region src/harness/detect-harness.d.ts
75
- export declare function detectHarness(payload: Readonly<Record<string, unknown>>): Harness | null;
76
- //#endregion
77
- //#region src/harness/parse-payload.d.ts
78
- export declare function parsePayload(body: unknown): HookPayload | null;
79
- //#endregion
80
- //#region src/harness/render-verdict.d.ts
81
- export declare function renderVerdict(event: HookEvent, verdict: Verdict): string | null;
82
- //#endregion
83
- //#region src/install/hook-config.d.ts
84
- export declare function buildHookConfig(harness: Harness, command: string, event?: HookEvent): string;
85
- export declare const SETTINGS_PATHS: Readonly<Record<Harness, string>>;
86
- //#endregion
87
68
  //#region src/model/types.d.ts
88
69
  interface DecisionRule {
89
70
  readonly name: string;
@@ -117,6 +98,19 @@ interface DecisionDiagnostics {
117
98
  }[];
118
99
  }
119
100
  //#endregion
101
+ //#region src/classify-action.d.ts
102
+ interface ActionOutcome {
103
+ readonly verdict: Verdict | null;
104
+ readonly note: string;
105
+ readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
106
+ readonly unavailable?: boolean;
107
+ readonly diagnostics?: DecisionDiagnostics;
108
+ }
109
+ interface ClassifyOptions extends EvaluationOptions {
110
+ readonly localOnly?: boolean;
111
+ }
112
+ export declare function classifyAction(request: ActionRequest, config: Config, options?: ClassifyOptions): Promise<ActionOutcome>;
113
+ //#endregion
120
114
  //#region src/model/classify-with-model.d.ts
121
115
  interface ModelOutcome {
122
116
  readonly verdict: Verdict | null;
@@ -124,7 +118,7 @@ interface ModelOutcome {
124
118
  readonly unavailable?: boolean;
125
119
  readonly diagnostics?: DecisionDiagnostics;
126
120
  }
127
- export declare function classifyWithModel(payload: HookPayload, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
121
+ export declare function classifyWithModel(payload: ActionRequest, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
128
122
  //#endregion
129
123
  //#region src/model/parse-verdict.d.ts
130
124
  export declare function parseModelVerdict(text: string): Verdict;
@@ -136,6 +130,12 @@ interface PolicyPaths {
136
130
  }
137
131
  export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
138
132
  //#endregion
133
+ //#region src/request/parse-action-request.d.ts
134
+ export declare function parseActionRequest(body: unknown): ActionRequest | null;
135
+ //#endregion
136
+ //#region src/request/render-verdict.d.ts
137
+ export declare function renderVerdict(verdict: Verdict): string | null;
138
+ //#endregion
139
139
  //#region src/rules/classify-locally.d.ts
140
140
  type LocalVerdict = {
141
141
  readonly kind: 'allow';
@@ -143,13 +143,6 @@ type LocalVerdict = {
143
143
  } | {
144
144
  readonly kind: 'escalate';
145
145
  };
146
- export declare function classifyLocally(payload: HookPayload): LocalVerdict;
147
- //#endregion
148
- //#region src/transcript/read-transcript.d.ts
149
- interface TranscriptEntry {
150
- readonly role: string;
151
- readonly text: string;
152
- }
153
- export declare function readTranscript(path: string | undefined, limit: number): Promise<readonly TranscriptEntry[]>;
146
+ export declare function classifyLocally(payload: ActionRequest): LocalVerdict;
154
147
  //#endregion
155
- export type { Config, Harness, HookEvent, HookPayload, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, TranscriptEntry, Verdict };
148
+ export type { ActionOutcome, ActionRequest, ClassifyOptions, Config, DecisionContext, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, Verdict };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as resolveApiKey, a as readTranscript, c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, h as PRESETS, m as DEFAULT_CONFIG, n as classifyWithModel, o as loadPolicy, p as detectHarness, r as parseModelVerdict, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-BdXQYjfj.js";
2
- export { DEFAULT_CONFIG, PRESETS, SETTINGS_PATHS, buildHookConfig, classifyLocally, classifyWithModel, detectHarness, loadConfig, loadPolicy, parseModelVerdict, parsePayload, readTranscript, renderVerdict, resolveApiKey, resolveConfigPath };
1
+ import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, l as PRESETS, n as parseActionRequest, o as parseModelVerdict, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-FdG6gahX.js";
2
+ export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };