handsoff 0.1.1 → 0.1.2-beta.1

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.
@@ -2,20 +2,9 @@
2
2
  # Copy to ~/.handsoff/config.toml and fill in your credentials
3
3
 
4
4
  [general]
5
- default_agent = "claude"
6
5
  log_level = "info"
7
6
  hook_server_port = 9876
8
7
 
9
- [channel.telegram]
10
- enabled = true
11
- bot_token = "YOUR_BOT_TOKEN"
12
- allowed_users = [123456789]
13
-
14
- [channel.feishu]
15
- enabled = false
16
- app_id = ""
17
- app_secret = ""
18
-
19
8
  [agent.claude]
20
9
  adapter = "hooks"
21
10
  binary = "claude"
@@ -23,22 +12,26 @@ work_dir = ""
23
12
  model = ""
24
13
  permission_mode = ""
25
14
 
26
- # Agent-specific permission settings
27
- [agent.claude.permission]
28
- # Low-risk tools that won't trigger permission prompts
29
- lowRiskTools = ["Read", "Glob", "Grep", "List", "WebSearch", "codesearch"]
15
+ [agent.codex]
16
+ enabled = false
17
+ command = ["codex", "app-server", "--listen", "stdio://"]
18
+ work_dir = ""
19
+ model = ""
20
+ approval_policy = ""
30
21
 
31
- [channels.app]
22
+ [channel.app]
32
23
  enabled = false
33
24
  channel_id = "app:desktop-001"
34
25
  auth_token = "change-me"
35
26
  notify_types = ["*"]
36
27
  heartbeat_interval_ms = 30000
37
28
 
38
- [agent.codex]
29
+ [channel.telegram]
30
+ enabled = true
31
+ bot_token = "YOUR_BOT_TOKEN"
32
+ allowed_users = [123456789]
33
+
34
+ [channel.feishu]
39
35
  enabled = false
40
- command = ["codex", "app-server", "--listen", "stdio://"]
41
- work_dir = ""
42
- model = ""
43
- approval_policy = ""
44
- sandbox_mode = ""
36
+ app_id = ""
37
+ app_secret = ""
package/dist/cli/index.js CHANGED
@@ -8,7 +8,13 @@ import { Command } from "commander";
8
8
 
9
9
  // src/shared/settings-merge.ts
10
10
  function isHandsoffHook(hook) {
11
- return hook.type === "http" && hook.url.includes("/hook/");
11
+ if (hook.type === "http" && hook.url) {
12
+ return hook.url.includes("/hook/");
13
+ }
14
+ if (hook.type === "command" && hook.command) {
15
+ return hook.command.includes("/hook/") && hook.command.includes("curl");
16
+ }
17
+ return false;
12
18
  }
13
19
  function isHandsoffEntry(entry) {
14
20
  return entry.hooks.some((hook) => isHandsoffHook(hook));
@@ -35,10 +41,15 @@ function mergeHooks(existing, newHooks) {
35
41
  const existingEntries = cleaned.hooks?.[eventName] || [];
36
42
  const newEntries = newHooks[eventName] || [];
37
43
  const existingUrls = new Set(
38
- existingEntries.flatMap((e) => e.hooks.map((h) => h.url))
44
+ existingEntries.flatMap((e) => e.hooks.map((h) => h.url).filter(Boolean))
45
+ );
46
+ const existingCommands = new Set(
47
+ existingEntries.flatMap((e) => e.hooks.map((h) => h.command).filter(Boolean))
39
48
  );
40
49
  const uniqueNewEntries = newEntries.filter(
41
- (entry) => !entry.hooks.some((h) => existingUrls.has(h.url))
50
+ (entry) => !entry.hooks.some(
51
+ (h) => h.url && existingUrls.has(h.url) || h.command && existingCommands.has(h.command)
52
+ )
42
53
  );
43
54
  const merged = [...uniqueNewEntries, ...existingEntries];
44
55
  if (merged.length > 0) {
@@ -124,7 +135,6 @@ import dotenv from "dotenv";
124
135
  dotenv.config();
125
136
  var DEFAULT_CONFIG = {
126
137
  general: {
127
- default_agent: "claude",
128
138
  log_level: "info",
129
139
  hook_server_port: 9876
130
140
  },
@@ -148,6 +158,11 @@ var DEFAULT_CONFIG = {
148
158
  timeout_ms: 3e5,
149
159
  // 5 minutes
150
160
  default_on_timeout: "deny"
161
+ },
162
+ app: {
163
+ enabled: false,
164
+ channel_id: "",
165
+ auth_token: ""
151
166
  }
152
167
  },
153
168
  agent: {
@@ -156,21 +171,16 @@ var DEFAULT_CONFIG = {
156
171
  binary: "claude",
157
172
  work_dir: "",
158
173
  model: "",
159
- permission_mode: "",
160
- permission: {
161
- lowRiskTools: ["Read", "Glob", "Grep", "List", "WebSearch", "codesearch"]
162
- }
174
+ permission_mode: ""
163
175
  },
164
176
  codex: {
165
177
  enabled: false,
166
178
  command: ["codex", "app-server", "--listen", "stdio://"],
167
179
  work_dir: "",
168
180
  model: "",
169
- approval_policy: "",
170
- sandbox_mode: ""
181
+ approval_policy: ""
171
182
  }
172
183
  },
173
- channels: {},
174
184
  bindings: {}
175
185
  };
176
186
  function getConfigPath() {
@@ -218,16 +228,21 @@ function mergeWithDefaults(raw) {
218
228
  10
219
229
  ),
220
230
  default_on_timeout: process.env.HANDSOFF_PERMISSION_DEFAULT || raw.channel?.permission?.default_on_timeout || DEFAULT_CONFIG.channel.permission.default_on_timeout
231
+ },
232
+ app: {
233
+ ...DEFAULT_CONFIG.channel.app,
234
+ ...raw.channel?.app || {},
235
+ enabled: raw.channel?.app?.enabled === true,
236
+ channel_id: String(raw.channel?.app?.channel_id || ""),
237
+ auth_token: String(raw.channel?.app?.auth_token || ""),
238
+ heartbeat_interval_ms: raw.channel?.app?.heartbeat_interval_ms ? parseInt(String(raw.channel?.app?.heartbeat_interval_ms), 10) : void 0,
239
+ notify_types: Array.isArray(raw.channel?.app?.notify_types) ? raw.channel.app.notify_types.map(String) : void 0
221
240
  }
222
241
  },
223
242
  agent: {
224
243
  claude: {
225
244
  ...DEFAULT_CONFIG.agent.claude,
226
- ...raw.agent?.claude || {},
227
- permission: {
228
- ...DEFAULT_CONFIG.agent.claude.permission,
229
- ...raw.agent?.claude?.permission || {}
230
- }
245
+ ...raw.agent?.claude || {}
231
246
  },
232
247
  codex: {
233
248
  ...DEFAULT_CONFIG.agent.codex,
@@ -235,21 +250,9 @@ function mergeWithDefaults(raw) {
235
250
  command: raw.agent?.codex?.command ? raw.agent.codex.command : DEFAULT_CONFIG.agent.codex.command,
236
251
  work_dir: raw.agent?.codex?.work_dir || DEFAULT_CONFIG.agent.codex.work_dir,
237
252
  model: raw.agent?.codex?.model || DEFAULT_CONFIG.agent.codex.model,
238
- approval_policy: raw.agent?.codex?.approval_policy || DEFAULT_CONFIG.agent.codex.approval_policy,
239
- sandbox_mode: raw.agent?.codex?.sandbox_mode || DEFAULT_CONFIG.agent.codex.sandbox_mode
253
+ approval_policy: raw.agent?.codex?.approval_policy || DEFAULT_CONFIG.agent.codex.approval_policy
240
254
  }
241
255
  },
242
- channels: {
243
- ...DEFAULT_CONFIG.channels,
244
- ...raw.channels || {},
245
- app: raw.channels?.app ? {
246
- enabled: raw.channels.app.enabled === true,
247
- channel_id: String(raw.channels.app.channel_id || ""),
248
- auth_token: String(raw.channels.app.auth_token || ""),
249
- heartbeat_interval_ms: raw.channels.app.heartbeat_interval_ms ? parseInt(String(raw.channels.app.heartbeat_interval_ms), 10) : void 0,
250
- notify_types: Array.isArray(raw.channels.app.notify_types) ? raw.channels.app.notify_types.map(String) : void 0
251
- } : DEFAULT_CONFIG.channels?.app
252
- },
253
256
  bindings: {
254
257
  ...DEFAULT_CONFIG.bindings,
255
258
  ...raw.bindings || {}
@@ -296,7 +299,11 @@ var en_default = {
296
299
  noHooksSelected: "No hooks selected, skipping injection.",
297
300
  injecting: "Injecting hooks...",
298
301
  injected: "Hooks injected",
299
- failed: "Failed: {{error}}"
302
+ failed: "Failed: {{error}}",
303
+ remoteModeSkipHooks: "Remote mode does not require hooks; skipping injection.",
304
+ cleaningHooks: "Cleaning up hooks from previous mode...",
305
+ hooksCleaned: "Hooks cleaned",
306
+ cleanupWarning: "Hook cleanup warning: {{error}}"
300
307
  },
301
308
  channel: {
302
309
  configuring: "Configuring {{channel}}",
@@ -515,6 +522,22 @@ var en_default = {
515
522
  unauthorized: "You are not authorized to use this bot.",
516
523
  received: "Message received.",
517
524
  processingError: "Error processing message."
525
+ },
526
+ eventTitles: {
527
+ sessionStart: "\u{1F680} Session Started",
528
+ sessionEnd: "\u{1F44B} Session Ended",
529
+ turnFinished: "\u2705 Task Completed",
530
+ turnThinking: "\u{1F4AD} Thinking...",
531
+ toolPost: "\u{1F527} Tool Call",
532
+ toolExecuted: "\u2705 Tool Executed",
533
+ toolFailure: "\u274C Tool Failed",
534
+ permissionRequest: "\u{1F510} Permission Request",
535
+ permissionResponse: "\u{1F4CB} Permission Response",
536
+ questionRequest: "\u2753 Question",
537
+ questionResponse: "\u{1F4CB} Question Response",
538
+ agentMessage: "\u{1F916} Agent Message",
539
+ error: "\u26A0\uFE0F Error",
540
+ fallback: "\u{1F4CB} Notification"
518
541
  }
519
542
  };
520
543
 
@@ -668,12 +691,18 @@ var ConfigApplicator = class {
668
691
  };
669
692
  let finalConfig = merged;
670
693
  if (bindings && Object.keys(bindings).length > 0) {
694
+ const finalBindings = { ...merged.bindings || {} };
695
+ for (const [agent, channelId] of Object.entries(bindings)) {
696
+ for (const [a, cid] of Object.entries(finalBindings)) {
697
+ if (cid === channelId && a !== agent) {
698
+ delete finalBindings[a];
699
+ }
700
+ }
701
+ finalBindings[agent] = channelId;
702
+ }
671
703
  finalConfig = {
672
704
  ...merged,
673
- bindings: {
674
- ...merged.bindings || {},
675
- ...bindings
676
- }
705
+ bindings: finalBindings
677
706
  };
678
707
  }
679
708
  const configDir = join4(homedir4(), ".handsoff");
@@ -776,6 +805,16 @@ function extractHandsoffHookInfo(entry) {
776
805
  }
777
806
  return { isHandsoff: false };
778
807
  }
808
+ function detectPermissionMode() {
809
+ const settingsPath = join5(homedir5(), ".claude", "settings.json");
810
+ if (!existsSync4(settingsPath)) return null;
811
+ try {
812
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
813
+ return settings.permissionMode ?? null;
814
+ } catch {
815
+ return null;
816
+ }
817
+ }
779
818
 
780
819
  // src/cli/wizard/detectors/codex.ts
781
820
  import { execSync as execSync2 } from "child_process";
@@ -850,6 +889,25 @@ var prompts = {
850
889
  { name: t("wizard.cli.backAction"), value: "back" }
851
890
  ]
852
891
  }),
892
+ claudeMode: () => select({
893
+ message: "Choose Claude integration mode:",
894
+ choices: [
895
+ { name: "Hook mode (local one-way)", value: "hooks" },
896
+ { name: "Remote mode (managed two-way)", value: "remote" }
897
+ ]
898
+ }),
899
+ permissionModeSelect: (defaultValue) => select({
900
+ message: "Permission Mode",
901
+ choices: [
902
+ { name: "acceptEdits \u2014 Auto-approve safe edits (Recommended)", value: "acceptEdits" },
903
+ { name: "default \u2014 Ask via TTY or hook", value: "default" },
904
+ { name: "bypassPermissions \u2014 Approve all, no prompts", value: "bypassPermissions" },
905
+ { name: "dontAsk \u2014 Deny all tool execution", value: "dontAsk" },
906
+ { name: "plan \u2014 Plan only, no tool execution", value: "plan" },
907
+ { name: "auto \u2014 Auto-handle based on risk level", value: "auto" }
908
+ ],
909
+ default: defaultValue ?? "acceptEdits"
910
+ }),
853
911
  cliHookSelect: (selected) => checkbox({
854
912
  message: t("wizard.cli.hookCheckbox"),
855
913
  choices: [
@@ -886,10 +944,10 @@ var prompts = {
886
944
  notifyTypes: (selected) => checkbox({
887
945
  message: t("wizard.notify.checkbox"),
888
946
  choices: [
889
- { name: "permission_request", value: "permission_request", checked: selected?.includes("permission_request") },
890
- { name: "question_request", value: "question_request", checked: selected?.includes("question_request") },
891
- { name: "finished", value: "finished", checked: selected?.includes("finished") },
892
- { name: "session_start", value: "session_start", checked: selected?.includes("session_start") },
947
+ { name: "permission:request", value: "permission:request", checked: selected?.includes("permission:request") },
948
+ { name: "question:request", value: "question:request", checked: selected?.includes("question:request") },
949
+ { name: "turn:finished", value: "turn:finished", checked: selected?.includes("turn:finished") },
950
+ { name: "session:start", value: "session:start", checked: selected?.includes("session:start") },
893
951
  { name: "error", value: "error", checked: selected?.includes("error") }
894
952
  ]
895
953
  }),
@@ -919,8 +977,8 @@ var prompts = {
919
977
  pairContinue: () => select({
920
978
  message: "Add another pair?",
921
979
  choices: [
922
- { name: "Add another pair", value: "continue" },
923
- { name: "Finish setup", value: "finish" }
980
+ { name: "Finish setup", value: "finish" },
981
+ { name: "Add another pair", value: "continue" }
924
982
  ]
925
983
  }),
926
984
  agentSelect: (agents) => select({
@@ -938,6 +996,7 @@ import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSyn
938
996
  import { dirname as dirname3 } from "path";
939
997
  import pc from "picocolors";
940
998
  import ora from "ora";
999
+ import { execSync as execSync3 } from "child_process";
941
1000
  var STEP = "STEP 2";
942
1001
  async function stepCli(state) {
943
1002
  const cliName = state.currentCliName || "claude";
@@ -967,34 +1026,58 @@ ${STEP} \u2014 Configure Agent
967
1026
  console.log(pc.yellow(` ${t("wizard.cli.hooksNotInstalled")}`));
968
1027
  }
969
1028
  console.log("");
970
- const action = await prompts.cliActionSelect(detection.hooked);
971
- if (action === "back") {
972
- console.log("");
973
- return { action: "next", next: "cli-menu" };
974
- }
975
- const selectedHooks = await prompts.cliHookSelect();
976
- if (selectedHooks.length === 0) {
977
- console.log(pc.yellow(`! ${t("wizard.cli.noHooksSelected")}
1029
+ const mode = await prompts.claudeMode();
1030
+ if (mode === "hooks") {
1031
+ const action = await prompts.cliActionSelect(detection.hooked);
1032
+ if (action === "back") {
1033
+ console.log("");
1034
+ return { action: "next", next: "cli-menu" };
1035
+ }
1036
+ const selectedHooks = await prompts.cliHookSelect();
1037
+ if (selectedHooks.length === 0) {
1038
+ console.log(pc.yellow(`! ${t("wizard.cli.noHooksSelected")}
978
1039
  `));
979
- return { action: "next", next: "cli-menu" };
1040
+ return { action: "next", next: "cli-menu" };
1041
+ }
1042
+ const spinner = ora(t("wizard.cli.injecting")).start();
1043
+ try {
1044
+ injectClaudeHooks(detection.settingsPath, selectedHooks);
1045
+ spinner.succeed(t("wizard.cli.injected"));
1046
+ } catch (err) {
1047
+ spinner.fail(t("wizard.cli.failed", { error: String(err) }));
1048
+ state.itemStatus.set("claude", "error");
1049
+ state.validationResults.set("claude", { ok: false, message: String(err) });
1050
+ return { action: "next", next: "cli-menu" };
1051
+ }
1052
+ } else {
1053
+ const spinner = ora(t("wizard.cli.cleaningHooks")).start();
1054
+ try {
1055
+ cleanupClaudeHooks(detection.settingsPath);
1056
+ spinner.succeed(t("wizard.cli.hooksCleaned"));
1057
+ } catch (err) {
1058
+ spinner.warn(t("wizard.cli.cleanupWarning", { error: String(err) }));
1059
+ }
1060
+ console.log(pc.yellow(`! ${t("wizard.cli.remoteModeSkipHooks")}`));
1061
+ try {
1062
+ execSync3("claude --version", { stdio: "ignore" });
1063
+ } catch {
1064
+ console.log(pc.yellow("! claude CLI not found in PATH; remote mode may not work until it is installed."));
1065
+ }
980
1066
  }
981
- const spinner = ora(t("wizard.cli.injecting")).start();
982
- try {
983
- injectClaudeHooks(detection.settingsPath, selectedHooks);
984
- spinner.succeed(t("wizard.cli.injected"));
985
- state.itemStatus.set("claude", "configured");
986
- state.sessionConfigured.add("claude");
987
- state.pendingChanges.agent = {
988
- ...state.pendingChanges.agent || {},
989
- claude: { adapter: "hooks" }
990
- };
991
- state.validationResults.set("claude", { ok: true });
992
- return { action: "next", next: "channel-select" };
993
- } catch (err) {
994
- spinner.fail(t("wizard.cli.failed", { error: String(err) }));
995
- state.itemStatus.set("claude", "error");
996
- state.validationResults.set("claude", { ok: false, message: String(err) });
1067
+ state.itemStatus.set("claude", "configured");
1068
+ state.sessionConfigured.add("claude");
1069
+ state.pendingChanges.agent = {
1070
+ ...state.pendingChanges.agent || {},
1071
+ claude: {
1072
+ ...state.pendingChanges.agent?.claude || {},
1073
+ adapter: mode
1074
+ }
1075
+ };
1076
+ state.validationResults.set("claude", { ok: true });
1077
+ if (mode === "remote") {
1078
+ return { action: "next", next: "permission-mode" };
997
1079
  }
1080
+ return { action: "next", next: "channel-select" };
998
1081
  }
999
1082
  if (cliName === "codex") {
1000
1083
  const detection = state.codexDetection ?? detectCodex();
@@ -1058,8 +1141,7 @@ function parseCommandInput(input2) {
1058
1141
  function injectClaudeHooks(settingsPath, events) {
1059
1142
  const port = loadConfig().general.hook_server_port;
1060
1143
  const token = getOrCreateHookToken();
1061
- const baseUrl = `http://localhost:${port}/hook/${token}`;
1062
- const hooksConfig = generateHooksConfig2(baseUrl, events);
1144
+ const hooksConfig = generateHooksConfig2(port, token, events);
1063
1145
  let currentSettings = {};
1064
1146
  if (existsSync5(settingsPath)) {
1065
1147
  try {
@@ -1074,17 +1156,40 @@ function injectClaudeHooks(settingsPath, events) {
1074
1156
  mkdirSync4(dirname3(settingsPath), { recursive: true });
1075
1157
  writeFileSync4(settingsPath, JSON.stringify(mergedSettings, null, 2));
1076
1158
  }
1077
- function generateHooksConfig2(baseUrl, events) {
1078
- const createEntry = (event) => ({
1079
- matcher: "",
1080
- hooks: [{ type: "http", url: `${baseUrl}/${event}` }]
1081
- });
1159
+ var COMMAND_ONLY_HOOKS = ["SessionStart", "Setup"];
1160
+ function generateHooksConfig2(port, token, events) {
1161
+ const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;
1162
+ const curlCommand = `curl -s -X POST -H 'Content-Type: application/json' -d @- ${unifiedUrl}`;
1163
+ const createEntry = (event) => {
1164
+ const isCommandOnly = COMMAND_ONLY_HOOKS.includes(event);
1165
+ return {
1166
+ matcher: "",
1167
+ hooks: [{
1168
+ type: isCommandOnly ? "command" : "http",
1169
+ ...isCommandOnly ? { command: curlCommand } : { url: unifiedUrl }
1170
+ }]
1171
+ };
1172
+ };
1082
1173
  const config = {};
1083
1174
  for (const event of events) {
1084
1175
  config[event] = [createEntry(event)];
1085
1176
  }
1086
1177
  return config;
1087
1178
  }
1179
+ function cleanupClaudeHooks(settingsPath) {
1180
+ if (!existsSync5(settingsPath)) return;
1181
+ let currentSettings = {};
1182
+ try {
1183
+ currentSettings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
1184
+ } catch {
1185
+ }
1186
+ const cleanedSettings = removeHandsoffHooks(currentSettings);
1187
+ if (!cleanedSettings.hooks || Object.keys(cleanedSettings.hooks).length === 0) {
1188
+ delete cleanedSettings.hooks;
1189
+ }
1190
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
1191
+ writeFileSync4(settingsPath, JSON.stringify(cleanedSettings, null, 2));
1192
+ }
1088
1193
 
1089
1194
  // src/cli/wizard/detectors/channel.ts
1090
1195
  async function detectChannel(name) {
@@ -1314,7 +1419,7 @@ ${STEP3} \u2014 Select Agent
1314
1419
  if (result.next === "cli-menu") {
1315
1420
  return { action: "next", next: "agent-select" };
1316
1421
  }
1317
- return { action: "next", next: "channel-select" };
1422
+ return { action: "next", next: result.next };
1318
1423
  }
1319
1424
 
1320
1425
  // src/shared/channelInstance.ts
@@ -1344,7 +1449,6 @@ ${STEP4} \u2014 Select Channel
1344
1449
  const loggerInstanceId = "logger:default";
1345
1450
  const loggerBoundAgent = Object.entries(currentBindings).find(([, v]) => v === loggerInstanceId)?.[0];
1346
1451
  const loggerStatus = loggerBoundAgent ? pc6.green(`[bound to ${loggerBoundAgent}]`) : loggerEnabled ? pc6.green("[configured]") : pc6.gray("[not configured]");
1347
- channels.push({ name: "logger", value: "logger", status: loggerStatus });
1348
1452
  if (mergedTelegram?.enabled && mergedTelegram.bot_token) {
1349
1453
  const instanceId = getChannelInstanceId("telegram", mergedTelegram.bot_token);
1350
1454
  const boundAgent = Object.entries(currentBindings).find(([, v]) => v === instanceId)?.[0];
@@ -1361,6 +1465,7 @@ ${STEP4} \u2014 Select Channel
1361
1465
  } else {
1362
1466
  channels.push({ name: "feishu", value: "feishu", status: pc6.gray("[not configured]") });
1363
1467
  }
1468
+ channels.push({ name: "logger", value: "logger", status: loggerStatus });
1364
1469
  const choice = await prompts.channelSelect(channels);
1365
1470
  state.currentChannelName = choice;
1366
1471
  const result = await stepChannelConfig(state);
@@ -1690,6 +1795,33 @@ ${t("wizard.final.done")}
1690
1795
  return { action: "next", next: "done" };
1691
1796
  }
1692
1797
 
1798
+ // src/cli/wizard/steps/permission-mode.ts
1799
+ import pc10 from "picocolors";
1800
+ var STEP7 = "STEP 3";
1801
+ async function stepPermissionMode(state) {
1802
+ console.log(pc10.bold(`
1803
+ ${STEP7} \u2014 Permission Mode
1804
+ `));
1805
+ console.log(pc10.gray(`${"\u2500".repeat(50)}
1806
+ `));
1807
+ const config = loadConfig();
1808
+ const current = state.pendingChanges?.agent?.claude;
1809
+ const saved = current?.permission_mode ?? config.agent.claude?.permission_mode ?? null;
1810
+ const detected = detectPermissionMode();
1811
+ const initial = saved ?? detected ?? "acceptEdits";
1812
+ console.log(pc10.gray("Controls how Claude Code handles permission requests.\n"));
1813
+ if (detected && !saved) {
1814
+ console.log(pc10.gray(`Detected from ~/.claude/settings.json: ${detected}
1815
+ `));
1816
+ }
1817
+ const selected = await prompts.permissionModeSelect(initial);
1818
+ state.pendingChanges ??= {};
1819
+ state.pendingChanges.agent ??= {};
1820
+ state.pendingChanges.agent.claude ??= {};
1821
+ state.pendingChanges.agent.claude.permission_mode = selected;
1822
+ return { action: "next", next: "channel-select" };
1823
+ }
1824
+
1693
1825
  // src/cli/wizard/engine.ts
1694
1826
  var WizardEngine = class {
1695
1827
  state;
@@ -1750,6 +1882,8 @@ var WizardEngine = class {
1750
1882
  return await stepPairContinue(this.state);
1751
1883
  case "cli":
1752
1884
  return await stepCli(this.state);
1885
+ case "permission-mode":
1886
+ return await stepPermissionMode(this.state);
1753
1887
  case "cli-menu":
1754
1888
  return await stepCliMenu(this.state);
1755
1889
  case "channel-config":
@@ -2226,22 +2360,29 @@ function writeSettings2(settingsPath, settings) {
2226
2360
  }
2227
2361
  function generateHooksConfig3(port, token) {
2228
2362
  const unifiedUrl = `http://localhost:${port}/hook/${token}/event`;
2229
- const createHookEntry = () => ({
2363
+ const createHttpHookEntry = () => ({
2230
2364
  matcher: "",
2231
2365
  hooks: [{ type: "http", url: unifiedUrl }]
2232
2366
  });
2367
+ const curlCommand = `curl -s -X POST -H 'Content-Type: application/json' -d @- ${unifiedUrl}`;
2368
+ const createCommandHookEntry = () => ({
2369
+ matcher: "",
2370
+ hooks: [{ type: "command", command: curlCommand }]
2371
+ });
2233
2372
  return {
2234
- Stop: [createHookEntry()],
2235
- PostToolUse: [createHookEntry()],
2236
- PostToolUseFailure: [createHookEntry()],
2237
- Notification: [createHookEntry()],
2238
- PreToolUse: [createHookEntry()],
2239
- SubagentStart: [createHookEntry()],
2240
- SubagentStop: [createHookEntry()],
2241
- SessionStart: [createHookEntry()],
2242
- SessionEnd: [createHookEntry()],
2243
- UserPromptSubmit: [createHookEntry()],
2244
- PermissionRequest: [createHookEntry()]
2373
+ // HTTP type hooks
2374
+ Stop: [createHttpHookEntry()],
2375
+ PostToolUse: [createHttpHookEntry()],
2376
+ PostToolUseFailure: [createHttpHookEntry()],
2377
+ Notification: [createHttpHookEntry()],
2378
+ PreToolUse: [createHttpHookEntry()],
2379
+ SubagentStart: [createHttpHookEntry()],
2380
+ SubagentStop: [createHttpHookEntry()],
2381
+ SessionEnd: [createHttpHookEntry()],
2382
+ UserPromptSubmit: [createHttpHookEntry()],
2383
+ PermissionRequest: [createHttpHookEntry()],
2384
+ // Command type hooks (HTTP not supported by Claude)
2385
+ SessionStart: [createCommandHookEntry()]
2245
2386
  };
2246
2387
  }
2247
2388
  function registerClaudeCommand(program2) {
@@ -2309,7 +2450,7 @@ function registerClaudeCommand(program2) {
2309
2450
  import { spawn as spawn4 } from "child_process";
2310
2451
  import { homedir as homedir14 } from "os";
2311
2452
  import { join as join16 } from "path";
2312
- import { execSync as execSync3 } from "child_process";
2453
+ import { execSync as execSync4 } from "child_process";
2313
2454
 
2314
2455
  // src/shared/logger.ts
2315
2456
  import pino from "pino";
@@ -2387,7 +2528,7 @@ function createAgentLogger(agentType) {
2387
2528
  // src/cli/agent/codex.ts
2388
2529
  function isCodexInstalled() {
2389
2530
  try {
2390
- execSync3("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true });
2531
+ execSync4("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true });
2391
2532
  return true;
2392
2533
  } catch {
2393
2534
  return false;
@@ -2395,7 +2536,7 @@ function isCodexInstalled() {
2395
2536
  }
2396
2537
  function getCodexVersion() {
2397
2538
  try {
2398
- return execSync3("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true }).trim();
2539
+ return execSync4("codex --version", { encoding: "utf8", stdio: "pipe", windowsHide: true }).trim();
2399
2540
  } catch {
2400
2541
  return void 0;
2401
2542
  }