impel-cli 0.16.1 → 0.16.2

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
@@ -551,19 +551,37 @@ impel codex exec --json "review the current diff"
551
551
  switch their native profiles in place; the isolated launchers are the safer
552
552
  default when personal and Impel sessions should coexist.
553
553
 
554
+ Every Impel-managed Claude configuration uses the same strict sandbox defaults:
555
+ the isolated `impel claude` profile, each tenant's Claude Desktop Code profile,
556
+ the native profile while `impel use gateway claude` is active, and branded
557
+ gateway-only CLI profiles. App config version 12 makes existing tenant profiles
558
+ refresh onto this policy after the CLI update.
559
+
554
560
  ### `impel use gateway [claude|codex|all]` (alias `impel on`)
555
561
 
556
562
  Turns **on** gateway mode for the given tool (default `all`). Requires `impel auth`
557
563
  first.
558
564
 
559
565
  **Claude Code** — merges into `~/.claude/settings.json` (creating it if needed,
560
- leaving every other key — permissions, hooks, other env vars untouched):
566
+ leaving permissions, hooks, other env vars, and custom sandbox filesystem/network
567
+ rules untouched). Gateway mode enables Claude's strict auto-allow sandbox by
568
+ default: Bash runs inside the OS sandbox without routine prompts, the
569
+ `dangerouslyDisableSandbox` escape hatch is disabled, and supported platforms
570
+ fail closed if isolation cannot initialize. Native Windows keeps startup
571
+ available because Claude's sandbox is supported only on macOS, Linux, and WSL2.
561
572
 
562
573
  ```json
563
574
  {
564
575
  "apiKeyHelper": "impel token",
565
576
  "env": {
566
577
  "ANTHROPIC_BASE_URL": "https://gateway.useimpel.com/anthropic"
578
+ },
579
+ "sandbox": {
580
+ "enabled": true,
581
+ "failIfUnavailable": true,
582
+ "autoAllowBashIfSandboxed": true,
583
+ "allowUnsandboxedCommands": false,
584
+ "excludedCommands": []
567
585
  }
568
586
  }
569
587
  ```
@@ -597,9 +615,9 @@ refresh_interval_ms = 300000
597
615
  ```
598
616
 
599
617
  Before overwriting anything, `impel` **backs up** what was there (any prior
600
- `apiKeyHelper` / `ANTHROPIC_BASE_URL`, and the prior Codex `model_provider`
601
- value) into `~/.config/impel/config.json`, so `impel use account` can restore it
602
- exactly. If `config.toml` already has a `[model_providers.impel]` table that
618
+ `apiKeyHelper`, `ANTHROPIC_BASE_URL`, Claude sandbox policy, and the prior Codex
619
+ `model_provider` value) into `~/.config/impel/config.json`, so `impel use account`
620
+ can restore it exactly. If `config.toml` already has a `[model_providers.impel]` table that
603
621
  wasn't written by this tool, `impel` refuses to touch the file and tells you to
604
622
  resolve it manually rather than risk emitting invalid/duplicate TOML.
605
623
 
@@ -908,7 +926,7 @@ impel status # see where each tool stands right now
908
926
  ```
909
927
 
910
928
  How the clean revert is guaranteed: `impel on` records the pre-existing values
911
- (your own `apiKeyHelper` / `ANTHROPIC_BASE_URL`, and Codex's prior
929
+ (your own `apiKeyHelper` / `ANTHROPIC_BASE_URL` / Claude sandbox policy, and Codex's prior
912
930
  `model_provider`) into `~/.config/impel/config.json` **before** overwriting them,
913
931
  and only ever captures a real *account-mode* value (re-running `impel on` never
914
932
  clobbers the backup with Impel's own values). `impel off` removes only keys that
@@ -916,8 +934,9 @@ still point at Impel and restores those backups — so a full `on` → `off`
916
934
  round-trip leaves `settings.json` and `config.toml` byte-for-byte back to where
917
935
  they started, with all your unrelated keys and tables intact.
918
936
 
919
- If you'd rather revert by hand: delete `apiKeyHelper` + the Impel
920
- `ANTHROPIC_BASE_URL` from `~/.claude/settings.json`; delete the block between the
937
+ If you'd rather revert by hand: delete `apiKeyHelper`, the Impel
938
+ `ANTHROPIC_BASE_URL`, and the Impel-managed sandbox keys from
939
+ `~/.claude/settings.json`; delete the block between the
921
940
  `# >>> impel-cli managed block ... >>>` / `# <<< impel-cli managed block <<<`
922
941
  markers in `~/.codex/config.toml` and reset `model_provider`; and
923
942
  `rm -rf ~/.config/impel` to forget the PAT + gateway entirely.
@@ -969,6 +988,12 @@ OpenAI Responses front door for SDK/Eve traffic and may normalize the body
969
988
  - https://developers.openai.com/codex/auth
970
989
  - https://developers.openai.com/codex/ide
971
990
 
991
+ ## Sources consulted for the Claude sandbox config
992
+
993
+ - https://code.claude.com/docs/en/settings#sandbox-settings
994
+ - https://code.claude.com/docs/en/sandboxing#configure-the-sandbox-for-your-organization
995
+ - https://github.com/anthropics/claude-code/blob/v2.1.205/examples/settings/settings-bash-sandbox.json
996
+
972
997
  ## Sources consulted for concurrent tenant apps
973
998
 
974
999
  - Electron v43.1.0 release: https://github.com/electron/electron/releases/tag/v43.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -13,6 +13,7 @@ import { normalizeTenantId } from "./tenants.js";
13
13
  import { impelCliInvocation } from "./selfInvocation.js";
14
14
  import { crossAppModelsEnabled, redactSecretText } from "./config.js";
15
15
  import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
16
+ import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
16
17
 
17
18
  export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
18
19
  const CHATGPT_CONFIG_START = "# >>> impel app managed gateway >>>";
@@ -162,7 +163,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
162
163
 
163
164
  // Bump when the written config/manifest schema changes; a mismatch forces the
164
165
  // slow open path (and thus a full config rewrite) after a CLI update.
165
- export const CURRENT_CONFIG_VERSION = 11;
166
+ export const CURRENT_CONFIG_VERSION = 12;
166
167
 
167
168
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
168
169
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -576,6 +577,7 @@ export function installManagedAppFiles({
576
577
  if (target === "claude") {
577
578
  migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
578
579
  writeClaudeDefaultApp(paths);
580
+ writeClaudeCodeSettings(paths);
579
581
  writeClaudeConfig(paths, config, models);
580
582
  }
581
583
  else writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
@@ -859,6 +861,23 @@ function writeClaudeConfig(paths, config, models) {
859
861
  }, null, 2) + "\n", 0o600);
860
862
  }
861
863
 
864
+ function writeClaudeCodeSettings(paths) {
865
+ const settingsPath = path.join(paths.claude.userData, "settings.json");
866
+ let settings = {};
867
+ if (fs.existsSync(settingsPath)) {
868
+ try {
869
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
870
+ } catch {
871
+ throw new Error(`Claude Code settings are not valid JSON: ${settingsPath}`);
872
+ }
873
+ }
874
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
875
+ throw new Error(`Claude Code settings must contain a JSON object: ${settingsPath}`);
876
+ }
877
+ applyImpelClaudeSandbox(settings);
878
+ writeAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 0o600);
879
+ }
880
+
862
881
  function writeClaudeDefaultApp(paths) {
863
882
  const configPath = path.join(paths.claude.userData, "claude_desktop_config.json");
864
883
  let current = {};
@@ -0,0 +1,80 @@
1
+ const MANAGED_SANDBOX_KEYS = Object.freeze([
2
+ "enabled",
3
+ "failIfUnavailable",
4
+ "autoAllowBashIfSandboxed",
5
+ "allowUnsandboxedCommands",
6
+ "excludedCommands",
7
+ ]);
8
+
9
+ function isJsonObject(value) {
10
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
11
+ }
12
+
13
+ function sameJsonValue(left, right) {
14
+ return JSON.stringify(left) === JSON.stringify(right);
15
+ }
16
+
17
+ /**
18
+ * Claude Code's strict, auto-allow sandbox profile.
19
+ *
20
+ * Native Windows does not implement Claude's sandbox. Keep it enabled so the
21
+ * same profile becomes effective in WSL, but do not make native Windows fail
22
+ * at startup solely because the platform cannot initialize it.
23
+ */
24
+ export function impelClaudeSandboxPolicy(platform = process.platform) {
25
+ return {
26
+ enabled: true,
27
+ failIfUnavailable: platform !== "win32",
28
+ autoAllowBashIfSandboxed: true,
29
+ allowUnsandboxedCommands: false,
30
+ excludedCommands: [],
31
+ };
32
+ }
33
+
34
+ /** Apply the managed sandbox keys while preserving unrelated sandbox config. */
35
+ export function applyImpelClaudeSandbox(settings, platform = process.platform) {
36
+ const current = isJsonObject(settings.sandbox) ? settings.sandbox : {};
37
+ settings.sandbox = {
38
+ ...current,
39
+ ...impelClaudeSandboxPolicy(platform),
40
+ };
41
+ return settings.sandbox;
42
+ }
43
+
44
+ /** Capture the exact pre-Impel value for reversible native-profile switching. */
45
+ export function captureClaudeSandbox(settings) {
46
+ return Object.hasOwn(settings, "sandbox")
47
+ ? { present: true, value: settings.sandbox }
48
+ : { present: false };
49
+ }
50
+
51
+ /**
52
+ * Restore only managed keys that still hold Impel's values. User changes made
53
+ * while gateway mode was active, plus unrelated filesystem/network keys, win.
54
+ */
55
+ export function restoreClaudeSandbox(settings, snapshot, platform = process.platform) {
56
+ if (!isJsonObject(settings.sandbox)) return false;
57
+ const policy = impelClaudeSandboxPolicy(platform);
58
+ const prior = snapshot?.present === true && isJsonObject(snapshot.value)
59
+ ? snapshot.value
60
+ : null;
61
+ const sandbox = { ...settings.sandbox };
62
+ let changed = false;
63
+
64
+ for (const key of MANAGED_SANDBOX_KEYS) {
65
+ if (!Object.hasOwn(sandbox, key) || !sameJsonValue(sandbox[key], policy[key])) continue;
66
+ if (prior && Object.hasOwn(prior, key)) sandbox[key] = prior[key];
67
+ else delete sandbox[key];
68
+ changed = true;
69
+ }
70
+
71
+ if (!changed) return false;
72
+ if (Object.keys(sandbox).length > 0 || (snapshot?.present === true && prior)) {
73
+ settings.sandbox = sandbox;
74
+ } else if (snapshot?.present === true) {
75
+ settings.sandbox = snapshot.value;
76
+ } else {
77
+ delete settings.sandbox;
78
+ }
79
+ return true;
80
+ }
@@ -10,6 +10,11 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
 
12
12
  import { IMPEL_MANAGED_MCP_ENV, impelMcpInvocation } from "./selfInvocation.js";
13
+ import {
14
+ applyImpelClaudeSandbox,
15
+ captureClaudeSandbox,
16
+ restoreClaudeSandbox,
17
+ } from "./claudeSandbox.js";
13
18
 
14
19
  export const CLAUDE_DIR = path.join(os.homedir(), ".claude");
15
20
  export const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
@@ -123,12 +128,14 @@ export function applyClaudeGateway(gatewayUrl) {
123
128
 
124
129
  const priorApiKeyHelper = settings.apiKeyHelper;
125
130
  const priorBaseUrl = settings.env?.ANTHROPIC_BASE_URL;
131
+ const priorSandbox = captureClaudeSandbox(settings);
126
132
 
127
133
  settings.apiKeyHelper = IMPEL_API_KEY_HELPER;
128
134
  settings.env = {
129
135
  ...(settings.env || {}),
130
136
  ANTHROPIC_BASE_URL: impelClaudeBaseUrl(gatewayUrl),
131
137
  };
138
+ const sandbox = applyImpelClaudeSandbox(settings);
132
139
 
133
140
  writeSettings(settings);
134
141
  userConfig.mcpServers = {
@@ -143,6 +150,8 @@ export function applyClaudeGateway(gatewayUrl) {
143
150
  baseUrl: settings.env.ANTHROPIC_BASE_URL,
144
151
  priorApiKeyHelper,
145
152
  priorBaseUrl,
153
+ priorSandbox,
154
+ sandbox,
146
155
  };
147
156
  }
148
157
 
@@ -179,6 +188,9 @@ export function revertClaudeGateway(gatewayUrl, backup = {}) {
179
188
  changed = true;
180
189
  }
181
190
 
191
+ const restoredSandbox = restoreClaudeSandbox(settings, backup.sandbox);
192
+ if (restoredSandbox) changed = true;
193
+
182
194
  if (changed) writeSettings(settings);
183
195
 
184
196
  if (isImpelMcpServer(userConfig.mcpServers?.[IMPEL_MCP_SERVER_NAME])) {
@@ -201,6 +213,7 @@ export function revertClaudeGateway(gatewayUrl, backup = {}) {
201
213
  removedHelper,
202
214
  removedBaseUrl,
203
215
  removedMcpServer,
216
+ restoredSandbox,
204
217
  restoredApiKeyHelper: removedHelper && backup.apiKeyHelper != null ? backup.apiKeyHelper : null,
205
218
  restoredBaseUrl: removedBaseUrl && backup.ANTHROPIC_BASE_URL != null ? backup.ANTHROPIC_BASE_URL : null,
206
219
  };
@@ -11,6 +11,7 @@ import {
11
11
  import { CONFIG_DIR } from "./config.js";
12
12
  import { normalizeTenantId } from "./tenants.js";
13
13
  import { impelCliInvocation, impelMcpInvocation } from "./selfInvocation.js";
14
+ import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
14
15
 
15
16
  export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
16
17
 
@@ -118,6 +119,7 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
118
119
  delete managedEnvironment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY;
119
120
  }
120
121
  settings.env = managedEnvironment;
122
+ applyImpelClaudeSandbox(settings);
121
123
  userConfig.mcpServers = {
122
124
  ...(userConfig.mcpServers &&
123
125
  typeof userConfig.mcpServers === "object" &&
@@ -76,9 +76,13 @@ async function useGateway({ target, app }) {
76
76
  if (!isImpelClaudeBaseUrl(result.priorBaseUrl, gatewayUrl)) {
77
77
  backup.ANTHROPIC_BASE_URL = result.priorBaseUrl ?? null;
78
78
  }
79
+ if (!Object.hasOwn(backup, "sandbox")) {
80
+ backup.sandbox = result.priorSandbox;
81
+ }
79
82
  console.log(`Claude Code -> GATEWAY (${result.path})`);
80
83
  console.log(` apiKeyHelper = "${result.apiKeyHelper}"`);
81
84
  console.log(` env.ANTHROPIC_BASE_URL = "${result.baseUrl}"`);
85
+ console.log(` sandbox = strict auto-allow (${result.sandbox.failIfUnavailable ? "fail closed" : "platform fallback"})`);
82
86
  console.log(` MCP server = "impel" (direct Impel CLI invocation)`);
83
87
  }
84
88
 
@@ -167,6 +171,9 @@ function useAccount({ target, app }) {
167
171
  if (result.removedMcpServer) {
168
172
  console.log(` removed Impel MCP server`);
169
173
  }
174
+ if (result.restoredSandbox) {
175
+ console.log(` restored prior Claude sandbox settings`);
176
+ }
170
177
  }
171
178
  if (config?.backups) delete config.backups.claude;
172
179
  }
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import readline from "node:readline";
6
6
 
7
7
  import { nativeCommandInvocation } from "../nativeProcess.js";
8
+ import { applyImpelClaudeSandbox } from "../claudeSandbox.js";
8
9
 
9
10
  const SAFE_ID = /^[a-z][a-z0-9-]{2,31}$/u;
10
11
  const SAFE_NAMESPACE = /^[a-z][a-z0-9_-]{1,31}$/u;
@@ -301,6 +302,7 @@ export function createGatewayCli(options) {
301
302
  };
302
303
  delete settings.env.ANTHROPIC_API_KEY;
303
304
  delete settings.env.ANTHROPIC_AUTH_TOKEN;
305
+ applyImpelClaudeSandbox(settings);
304
306
  writePrivateFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
305
307
  return { configDir, settingsPath };
306
308
  }