agent-relay-runner 0.123.0 → 0.123.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.123.0",
3
+ "version": "0.123.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.123.0",
4
+ "version": "0.123.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -32,6 +32,7 @@ import {
32
32
  resolveProjectMcpServers,
33
33
  profileProvisioning,
34
34
  writeCodexHooksJson,
35
+ writeClaudeSettingsJson,
35
36
  } from "./provisioning";
36
37
  import {
37
38
  claudeMcpLaunchArgs,
@@ -325,6 +326,28 @@ export function stripSettingsArgs(args: string[]): string[] {
325
326
  return result;
326
327
  }
327
328
 
329
+ // #1096 — the settings.json relay WRITES into an isolated managed Claude home so the provisioned
330
+ // hooks (baseline guard included) land in the DOCUMENTED hook source Claude honors
331
+ // ($CLAUDE_CONFIG_DIR/settings.json — loaded under `--setting-sources "user"` and the default),
332
+ // rather than riding ONLY the inline `--settings` CLI flag. Claude does not document `--settings`
333
+ // as a hook source, and — unlike a file written straight to disk — that flag's JSON must survive
334
+ // shell/tmux launcher transit and passes Claude's "silently ignore settings that fail validation"
335
+ // gate; a written settings.json bypasses both, so the baseline guard can't be silently absent
336
+ // (the #1096 inert-at-runtime symptom). Returns null when:
337
+ // • there is NO isolated managed home (host base reuses the real ~/.claude — relay must not
338
+ // clobber it; that path keeps the inline --settings delivery), or
339
+ // • a caller --settings is present (the merge/suppression path owns hook delivery inline; see
340
+ // claudeSettingsArgs) — so the file and inline channels never DOUBLE-register the same hook.
341
+ // Both assembleClaude (to keep these hooks OUT of the inline --settings) and materializeClaudeAssets
342
+ // (to write the file) call this, so the launch and the disk agree.
343
+ export function claudeManagedHookSettings(config: RunnerSpawnConfig, configHome: string | undefined): { hooks: ProvisioningHookSet } | null {
344
+ if (!configHome) return null;
345
+ const hostDefaultArgs = profileUsesProviderHostGlobals(config) ? config.providerConfig.defaultArgs : [];
346
+ if (hasSettingsArg(hostDefaultArgs) || hasSettingsArg(config.providerArgs)) return null;
347
+ const hookSet = renderProvisionedHookSet(resolveProvisionedHooks(configHome, config));
348
+ return Object.keys(hookSet).length ? { hooks: hookSet } : null;
349
+ }
350
+
328
351
  // ── Instruction disposition (the vanilla floor's effect on the host instruction cascade) ──
329
352
 
330
353
  function repoInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
@@ -374,9 +397,15 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
374
397
  const hookHome = configHome ?? assetHome;
375
398
  const provisionedHooks = hookHome ? resolveProvisionedHooks(hookHome, config) : [];
376
399
  const provisionedHookSet = renderProvisionedHookSet(provisionedHooks);
400
+ // #1096 — for an isolated managed home with no caller --settings, the provisioned hooks are
401
+ // delivered through the managed settings.json (the documented, transit-safe hook source), so
402
+ // keep them OUT of the inline --settings to avoid double-registering the same guard. Host-base
403
+ // and caller-`--settings` launches keep inline delivery (see claudeManagedHookSettings).
404
+ const managedHookSettings = claudeManagedHookSettings(config, configHome);
405
+ const inlineHookSet = managedHookSettings ? {} : provisionedHookSet;
377
406
  const settings = claudeSettingsArgs(
378
407
  projectSettings.settings,
379
- provisionedHookSet,
408
+ inlineHookSet,
380
409
  provisionedHooks.map((h) => h.name),
381
410
  statusLine,
382
411
  hostDefaultArgs,
@@ -629,6 +658,10 @@ function materializeClaudeAssets(config: RunnerSpawnConfig): void {
629
658
  materializeResolvedAssets(hostSkills);
630
659
  materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name))));
631
660
  materializeResolvedAssets(resolveProvisionedHooks(configHome, config));
661
+ // #1096 — land the provisioned hooks in the managed home's settings.json (the documented
662
+ // hook source), matching the assembler's decision to keep them out of the inline --settings.
663
+ const managedHookSettings = claudeManagedHookSettings(config, configHome);
664
+ if (managedHookSettings) writeClaudeSettingsJson(configHome, managedHookSettings);
632
665
  }
633
666
  if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
634
667
  if (!configHome && assetHome) materializeResolvedAssets(resolveProvisionedHooks(assetHome, config));
@@ -357,6 +357,21 @@ export function writeCodexHooksJson(codexHome: string, hooks: ProvisioningHookSe
357
357
  writeFileSync(join(codexHome, "hooks.json"), JSON.stringify({ hooks }, null, 2), { mode: 0o600 });
358
358
  }
359
359
 
360
+ // #1096 — write relay-owned settings into an ISOLATED Claude managed home's `settings.json`
361
+ // (the DOCUMENTED hook source Claude honors under `--setting-sources "user"` or the default,
362
+ // unlike the inline `--settings` CLI flag). The managed home is relay-owned and freshly keyed
363
+ // per spawn, so an unconditional write is safe (profile-home seeds only `.claude.json`/`CLAUDE.md`,
364
+ // never `settings.json`). Removes the file when there is nothing relay-owned to deliver so disk
365
+ // stays == report. Never called for a host-base launch (which reuses the real `~/.claude`).
366
+ export function writeClaudeSettingsJson(claudeHome: string, settings: Record<string, unknown>): void {
367
+ mkdirSync(claudeHome, { recursive: true });
368
+ if (!Object.keys(settings).length) {
369
+ rmSync(join(claudeHome, "settings.json"), { force: true });
370
+ return;
371
+ }
372
+ writeFileSync(join(claudeHome, "settings.json"), JSON.stringify(settings, null, 2), { mode: 0o600 });
373
+ }
374
+
360
375
  // Write the resolved assets to disk; returns the dirs that materialized successfully,
361
376
  // in resolution order. Idempotent — safe to re-run against an existing home.
362
377
  export function materializeResolvedAssets(assets: ResolvedAsset[]): string[] {