auto-mode 0.2.0 → 0.3.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
@@ -113,6 +113,10 @@ was there. Set `onFailure` to `deny` to fail closed instead.
113
113
  | `auto-mode print-prompt` | Print the assembled system prompt |
114
114
  | `auto-mode init <harness>` | Print the hook entry to add |
115
115
 
116
+ `init` takes `--event permission-request` to register on the event Claude Code sends when it is
117
+ about to ask you, rather than on every tool call. The
118
+ [harness guide](./docs/guides/harnesses.md#which-event) covers the choice.
119
+
116
120
  ## Documentation
117
121
 
118
122
  - [Overview](./docs/architecture/overview.md) — the two tiers, harness detection, the verdict
@@ -5663,18 +5663,30 @@ function F(t) {
5663
5663
  //#endregion
5664
5664
  //#region src/harness/render-verdict.ts
5665
5665
  function renderVerdict(event, verdict) {
5666
- const decision = M(verdict).with({ kind: "ask" }, () => ({ permissionDecision: "ask" })).with({ kind: "allow" }, () => ({ permissionDecision: "allow" })).with({ kind: "deny" }, (denied) => ({
5667
- permissionDecision: "deny",
5668
- permissionDecisionReason: `[${denied.rule}] ${denied.reason}`
5669
- })).exhaustive();
5670
- return JSON.stringify({ hookSpecificOutput: {
5666
+ const decision = M(event).with("PreToolUse", () => buildToolGateDecision(verdict)).with("PermissionRequest", () => buildPermissionDecision(verdict)).exhaustive();
5667
+ return decision === null ? null : JSON.stringify({ hookSpecificOutput: {
5671
5668
  hookEventName: event,
5672
5669
  ...decision
5673
5670
  } });
5674
5671
  }
5672
+ function buildToolGateDecision(verdict) {
5673
+ return M(verdict).with({ kind: "ask" }, () => ({ permissionDecision: "ask" })).with({ kind: "allow" }, () => ({ permissionDecision: "allow" })).with({ kind: "deny" }, (denied) => ({
5674
+ permissionDecision: "deny",
5675
+ permissionDecisionReason: formatReason(denied)
5676
+ })).exhaustive();
5677
+ }
5678
+ function buildPermissionDecision(verdict) {
5679
+ return M(verdict).with({ kind: "ask" }, () => null).with({ kind: "allow" }, () => ({ decision: { behavior: "allow" } })).with({ kind: "deny" }, (denied) => ({ decision: {
5680
+ behavior: "deny",
5681
+ message: formatReason(denied)
5682
+ } })).exhaustive();
5683
+ }
5684
+ function formatReason(denied) {
5685
+ return `[${denied.rule}] ${denied.reason}`;
5686
+ }
5675
5687
  //#endregion
5676
5688
  //#region src/install/hook-config.ts
5677
- function buildHookConfig(harness, command) {
5689
+ function buildHookConfig(harness, command, event = "PreToolUse") {
5678
5690
  const entry = M(harness).with("claude", () => ({
5679
5691
  matcher: ".*",
5680
5692
  hooks: [{
@@ -5693,13 +5705,17 @@ function buildHookConfig(harness, command) {
5693
5705
  command
5694
5706
  }]
5695
5707
  })).exhaustive();
5696
- return JSON.stringify({ hooks: { PreToolUse: [entry] } }, null, 2);
5708
+ return JSON.stringify({ hooks: { [event]: [entry] } }, null, 2);
5697
5709
  }
5698
5710
  const SETTINGS_PATHS = {
5699
5711
  claude: "~/.claude/settings.json",
5700
5712
  codex: "~/.codex/hooks.json",
5701
5713
  muse: "~/.config/muse/settings.json"
5702
5714
  };
5715
+ const EVENT_NOTES = {
5716
+ PreToolUse: ["The hook judges every tool call, including the ones the harness would have run", "without asking."],
5717
+ PermissionRequest: ["Claude Code sends this event only when it is about to ask you, so the hook", "answers those prompts and judges nothing the harness already allows."]
5718
+ };
5703
5719
  const SETUP_NOTES = {
5704
5720
  claude: ["A tool your permission settings already deny is refused before the hook sees it, so", "auto-mode can only narrow what the harness would have allowed."],
5705
5721
  codex: [
@@ -6200,4 +6216,4 @@ function toUnquoted(word) {
6200
6216
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
6201
6217
  }
6202
6218
  //#endregion
6203
- export { loadPolicy as a, buildHookConfig as c, detectHarness as d, DEFAULT_CONFIG as f, resolveConfigPath as g, resolveApiKey as h, readTranscript as i, renderVerdict as l, loadConfig as m, classifyWithModel as n, SETTINGS_PATHS as o, PRESETS as p, parseModelVerdict as r, SETUP_NOTES as s, classifyLocally as t, parsePayload as u };
6219
+ export { resolveConfigPath as _, loadPolicy as a, SETUP_NOTES as c, parsePayload as d, detectHarness as f, resolveApiKey as g, loadConfig as h, readTranscript as i, buildHookConfig as l, PRESETS as m, classifyWithModel as n, EVENT_NOTES as o, DEFAULT_CONFIG as p, parseModelVerdict as r, SETTINGS_PATHS as s, classifyLocally as t, renderVerdict as u };
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as loadPolicy, c as buildHookConfig, g as resolveConfigPath, l as renderVerdict, m as loadConfig, n as classifyWithModel, o as SETTINGS_PATHS, s as SETUP_NOTES, t as classifyLocally, u as parsePayload } from "./classify-locally-rTUwxBZi.js";
2
+ import { _ as resolveConfigPath, a as loadPolicy, c as SETUP_NOTES, d as parsePayload, h as loadConfig, l as buildHookConfig, n as classifyWithModel, o as EVENT_NOTES, s as SETTINGS_PATHS, t as classifyLocally, u as renderVerdict } from "./classify-locally-BRMllwcd.js";
3
3
  import { parseArgs } from "node:util";
4
4
  import { text } from "node:stream/consumers";
5
5
  //#region src/cli.ts
@@ -13,6 +13,7 @@ Usage:
13
13
  Options:
14
14
  --classifier <path> Use this framework file instead of the shipped one
15
15
  --rules <path> Use this rule list instead of the shipped one
16
+ --event <event> With init: pre-tool-use, the default, or permission-request
16
17
  --explain With run: also write the reasoning to stderr
17
18
  --local-only With run: skip the model tier
18
19
  `;
@@ -31,19 +32,27 @@ async function run(explain, localOnly) {
31
32
  if (payload === null) return printNote(explain, "not a tool gate this hook judges, so no verdict");
32
33
  const local = classifyLocally(payload);
33
34
  if (local.kind === "allow") {
34
- process.stdout.write(renderVerdict(payload.event, { kind: "allow" }));
35
+ writeVerdict(payload.event, { kind: "allow" });
35
36
  return printNote(explain, `allowed by ${local.exception} (${payload.harness}, local)`);
36
37
  }
37
38
  if (localOnly) return printNote(explain, `${payload.toolName} needs the model tier, which --local-only skipped`);
38
39
  const config = await loadConfig();
39
40
  const outcome = await classifyWithModel(payload, config);
40
- if (outcome.verdict !== null) process.stdout.write(renderVerdict(payload.event, outcome.verdict));
41
+ if (outcome.verdict !== null) writeVerdict(payload.event, outcome.verdict);
41
42
  return printNote(explain, outcome.note);
42
43
  }
44
+ function writeVerdict(event, verdict) {
45
+ const rendered = renderVerdict(event, verdict);
46
+ if (rendered !== null) process.stdout.write(rendered);
47
+ }
43
48
  function printNote(explain, message) {
44
49
  if (explain) process.stderr.write(`auto-mode: ${message}\n`);
45
50
  return 0;
46
51
  }
52
+ const HOOK_EVENTS = {
53
+ "pre-tool-use": "PreToolUse",
54
+ "permission-request": "PermissionRequest"
55
+ };
47
56
  async function main(argv) {
48
57
  const args = parseArgs({
49
58
  args: [...argv],
@@ -51,6 +60,7 @@ async function main(argv) {
51
60
  options: {
52
61
  classifier: { type: "string" },
53
62
  rules: { type: "string" },
63
+ event: { type: "string" },
54
64
  explain: { type: "boolean" },
55
65
  "local-only": { type: "boolean" },
56
66
  help: {
@@ -71,10 +81,19 @@ async function main(argv) {
71
81
  process.stderr.write("auto-mode init: name a harness — claude, codex or muse\n");
72
82
  return 2;
73
83
  }
84
+ const event = HOOK_EVENTS[args.values.event ?? "pre-tool-use"];
85
+ if (event === void 0) {
86
+ process.stderr.write("auto-mode init: --event takes pre-tool-use or permission-request\n");
87
+ return 2;
88
+ }
89
+ if (event === "PermissionRequest" && harness !== "claude") {
90
+ process.stderr.write(`auto-mode init: only Claude Code sends PermissionRequest\n`);
91
+ return 2;
92
+ }
74
93
  process.stdout.write(`# Add this to ${SETTINGS_PATHS[harness]}\n`);
75
94
  process.stdout.write(`# Configuration lives at ${resolveConfigPath()}\n`);
76
- process.stdout.write(`${buildHookConfig(harness, `${process.execPath} ${process.argv[1] ?? "auto-mode"} run`)}\n`);
77
- for (const line of SETUP_NOTES[harness]) process.stdout.write(`# ${line}\n`);
95
+ process.stdout.write(`${buildHookConfig(harness, `${process.execPath} ${process.argv[1] ?? "auto-mode"} run`, event)}\n`);
96
+ for (const line of [...SETUP_NOTES[harness], ...EVENT_NOTES[event]]) process.stdout.write(`# ${line}\n`);
78
97
  return 0;
79
98
  }
80
99
  if (command === "print-prompt") {
package/dist/index.d.ts CHANGED
@@ -51,10 +51,10 @@ export declare function detectHarness(payload: Readonly<Record<string, unknown>>
51
51
  export declare function parsePayload(body: unknown): HookPayload | null;
52
52
  //#endregion
53
53
  //#region src/harness/render-verdict.d.ts
54
- export declare function renderVerdict(event: HookEvent, verdict: Verdict): string;
54
+ export declare function renderVerdict(event: HookEvent, verdict: Verdict): string | null;
55
55
  //#endregion
56
56
  //#region src/install/hook-config.d.ts
57
- export declare function buildHookConfig(harness: Harness, command: string): string;
57
+ export declare function buildHookConfig(harness: Harness, command: string, event?: HookEvent): string;
58
58
  export declare const SETTINGS_PATHS: Readonly<Record<Harness, string>>;
59
59
  //#endregion
60
60
  //#region src/model/classify-with-model.d.ts
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as loadPolicy, c as buildHookConfig, d as detectHarness, f as DEFAULT_CONFIG, g as resolveConfigPath, h as resolveApiKey, i as readTranscript, l as renderVerdict, m as loadConfig, n as classifyWithModel, o as SETTINGS_PATHS, p as PRESETS, r as parseModelVerdict, t as classifyLocally, u as parsePayload } from "./classify-locally-rTUwxBZi.js";
1
+ import { _ as resolveConfigPath, a as loadPolicy, d as parsePayload, f as detectHarness, g as resolveApiKey, h as loadConfig, i as readTranscript, l as buildHookConfig, m as PRESETS, n as classifyWithModel, p as DEFAULT_CONFIG, r as parseModelVerdict, s as SETTINGS_PATHS, t as classifyLocally, u as renderVerdict } from "./classify-locally-BRMllwcd.js";
2
2
  export { DEFAULT_CONFIG, PRESETS, SETTINGS_PATHS, buildHookConfig, classifyLocally, classifyWithModel, detectHarness, loadConfig, loadPolicy, parseModelVerdict, parsePayload, readTranscript, renderVerdict, resolveApiKey, resolveConfigPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A permission classifier that runs as a hook, so agents that are not Claude can run unattended",
5
5
  "keywords": [
6
6
  "agent",