@orchid-labs/pluxx 0.1.15 → 0.1.16

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.
Files changed (45) hide show
  1. package/dist/claude-hook-probe.d.ts +70 -0
  2. package/dist/claude-hook-probe.d.ts.map +1 -0
  3. package/dist/cli/behavioral.d.ts +1 -0
  4. package/dist/cli/behavioral.d.ts.map +1 -1
  5. package/dist/cli/discover-installed-mcp.d.ts +1 -0
  6. package/dist/cli/discover-installed-mcp.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +4 -1
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/index.js +7225 -6161
  10. package/dist/cli/install.d.ts +1 -0
  11. package/dist/cli/install.d.ts.map +1 -1
  12. package/dist/cli/lint.d.ts.map +1 -1
  13. package/dist/cli/migrate.d.ts.map +1 -1
  14. package/dist/cli/verify-install.d.ts +7 -0
  15. package/dist/cli/verify-install.d.ts.map +1 -1
  16. package/dist/codex-agent-probe-shared.d.ts +26 -0
  17. package/dist/codex-agent-probe-shared.d.ts.map +1 -0
  18. package/dist/codex-agent-probe.d.ts +82 -0
  19. package/dist/codex-agent-probe.d.ts.map +1 -0
  20. package/dist/codex-exec-runner.d.ts +21 -0
  21. package/dist/codex-exec-runner.d.ts.map +1 -0
  22. package/dist/codex-hook-probe.d.ts +41 -0
  23. package/dist/codex-hook-probe.d.ts.map +1 -0
  24. package/dist/codex-hooks-feature.d.ts +10 -0
  25. package/dist/codex-hooks-feature.d.ts.map +1 -0
  26. package/dist/codex-interactive-agent-probe.d.ts +62 -0
  27. package/dist/codex-interactive-agent-probe.d.ts.map +1 -0
  28. package/dist/codex-interactive-hook-probe.d.ts +90 -0
  29. package/dist/codex-interactive-hook-probe.d.ts.map +1 -0
  30. package/dist/codex-interactive-probe-shared.d.ts +4 -0
  31. package/dist/codex-interactive-probe-shared.d.ts.map +1 -0
  32. package/dist/codex-mcp-probe.d.ts +77 -0
  33. package/dist/codex-mcp-probe.d.ts.map +1 -0
  34. package/dist/codex-permissions-companion.d.ts +19 -0
  35. package/dist/codex-permissions-companion.d.ts.map +1 -0
  36. package/dist/codex-probe-shared.d.ts +3 -0
  37. package/dist/codex-probe-shared.d.ts.map +1 -0
  38. package/dist/compiler-intent.d.ts +6 -6
  39. package/dist/generators/codex/index.d.ts +1 -0
  40. package/dist/generators/codex/index.d.ts.map +1 -1
  41. package/dist/hook-translation-registry.d.ts.map +1 -1
  42. package/dist/index.js +732 -26
  43. package/dist/toml-lite.d.ts +5 -0
  44. package/dist/toml-lite.d.ts.map +1 -0
  45. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -7855,7 +7855,7 @@ function getEnabledRuntimeReadinessBindings(capability, plan) {
7855
7855
  });
7856
7856
  }
7857
7857
  var NAMED_PROMPT_TARGET_NOTE = "Named `skills` / `commands` readiness targets currently translate through prompt-entry gating with best-effort matching because the core four do not share one exact per-skill or per-command runtime interception surface.";
7858
- var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, but Pluxx still emits `.codex/readiness.generated.json` and `.codex/hooks.generated.json` companion guidance because some Codex runtimes still gate hook activation behind `codex_hooks`.";
7858
+ var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because some Codex runtimes still gate hook activation behind a `[features]` flag. Current Codex config surfaces still accept both `hooks = true` and `codex_hooks = true`, but maintained interactive probes on May 13, 2026 showed local Codex CLI 0.130.0 timing out without a project-local hook side effect under either flag while emitting a deprecation warning for `codex_hooks` that points users to `hooks`.";
7859
7859
  function getRuntimeReadinessNamedPromptTargetNote() {
7860
7860
  return NAMED_PROMPT_TARGET_NOTE;
7861
7861
  }
@@ -7920,7 +7920,7 @@ function getRuntimeReadinessCapability(platform, pluginRootVar = "PLUGIN_ROOT")
7920
7920
  case "codex":
7921
7921
  return {
7922
7922
  platform,
7923
- delivery: "generated-guidance",
7923
+ delivery: "bundled-hooks",
7924
7924
  bundleEnforced: false,
7925
7925
  namedPromptTargetScope: "best-effort",
7926
7926
  scriptPath: ".codex/pluxx-readiness.mjs",
@@ -8024,7 +8024,7 @@ var PLATFORM_LIMITS = {
8024
8024
  manifestPromptCountMax: 3,
8025
8025
  manifestPathPrefix: "./",
8026
8026
  instructionsMaxBytes: 32768,
8027
- hooksFeatureFlag: "codex_hooks"
8027
+ hooksFeatureFlag: "hooks"
8028
8028
  },
8029
8029
  "cursor": {
8030
8030
  ...NULL_LIMITS,
@@ -8178,17 +8178,17 @@ var PLATFORM_VALIDATION_RULES = {
8178
8178
  notes: "The manifest is optional; if present, name is the only required field."
8179
8179
  },
8180
8180
  mcp: {
8181
- files: [".mcp.json", ".claude-plugin/plugin.json"],
8181
+ files: [".mcp.json", "~/.claude.json", "managed-mcp.json", ".claude-plugin/plugin.json"],
8182
8182
  rootKey: "mcpServers",
8183
8183
  transports: ["stdio", "http", "sse"],
8184
8184
  auth: ["headers", "env interpolation", "OAuth 2.0", "bearer tokens", "dynamic headers"],
8185
- notes: "Claude Code supports either inline MCP config in plugin.json or a separate .mcp.json file, with marketplace and dependency-aware install flows."
8185
+ notes: "Claude Code project-scoped MCP lives in .mcp.json, while local and user scopes live in ~/.claude.json; managed deployments can also pin managed-mcp.json. Current docs explicitly warn that settings.json does not read mcpServers."
8186
8186
  },
8187
8187
  hooks: {
8188
8188
  supported: true,
8189
- files: ["hooks/hooks.json", ".claude-plugin/plugin.json", "~/.claude/settings.json", ".claude/settings.json", ".claude/settings.local.json"],
8189
+ files: ["hooks/hooks.json", ".claude-plugin/plugin.json", "managed settings", "~/.claude/settings.json", ".claude/settings.json", ".claude/settings.local.json"],
8190
8190
  eventNames: ["SessionStart", "Setup", "UserPromptSubmit", "UserPromptExpansion", "PreToolUse", "PermissionRequest", "PermissionDenied", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "Notification", "SubagentStart", "SubagentStop", "TaskCreated", "TaskCompleted", "Stop", "StopFailure", "TeammateIdle", "InstructionsLoaded", "ConfigChange", "CwdChanged", "FileChanged", "WorktreeCreate", "WorktreeRemove", "PreCompact", "PostCompact", "Elicitation", "ElicitationResult", "SessionEnd"],
8191
- notes: "Hook configs can be stored in hooks/hooks.json, inlined in plugin.json, added in settings files, or scoped through skill and agent frontmatter. Claude also documents a broader lifecycle event set than the older simplified Pluxx model."
8191
+ notes: "Hook configs can be stored in hooks/hooks.json, inlined in plugin.json, added in user/project/local settings files, or controlled by managed policy settings; Claude also documents a broader lifecycle event set than the older simplified Pluxx model."
8192
8192
  },
8193
8193
  instructions: {
8194
8194
  files: ["CLAUDE.md"],
@@ -8304,7 +8304,7 @@ var PLATFORM_VALIDATION_RULES = {
8304
8304
  supported: true,
8305
8305
  files: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
8306
8306
  eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
8307
- notes: "Codex documents hooks in project and user config, and the hooks docs still mention the codex_hooks feature flag/runtime caveat. The official hooks docs also cover plugin-bundled hooks, which Pluxx now emits at hooks/hooks.json."
8307
+ notes: "Codex documents hooks in project and user config, and the current config schema still accepts both `[features].hooks = true` and `[features].codex_hooks = true`. Maintained interactive probes on May 13, 2026 showed local Codex CLI 0.130.0 timing out without a project-local `.codex/hooks.json` side effect or `/hooks` review gate under either flag; the `codex_hooks` variant also emitted a deprecation message that points users to `hooks`, while the official docs still show `codex_hooks`. The official hooks docs also cover plugin-bundled hooks, which Pluxx now emits at `hooks/hooks.json`."
8308
8308
  },
8309
8309
  instructions: {
8310
8310
  files: ["AGENTS.md", "AGENTS.override.md"],
@@ -8738,22 +8738,22 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8738
8738
  agents: {
8739
8739
  mode: "translate",
8740
8740
  nativeSurfaces: [".codex/agents/*.toml", "~/.codex/agents/*.toml", "subagent workflows"],
8741
- notes: "Codex custom agents and subagents are real native surfaces, but they are not packaged the same way as Claude or Cursor plugin agents."
8741
+ notes: 'Codex custom agents and subagents are real native surfaces, but they are not packaged the same way as Claude or Cursor plugin agents. Local May 13, 2026 headless probes now prove explicit invocation, built-in-name override, project-local precedence, and discovered `.agents/skills` inheritance. The same maintained headless suite also showed two config-depth caveats: a parent `[[skills.config]] enabled = false` entry did not disable a discovered project skill, and an agent-local `[[skills.config]]` entry did not preload an undiscovered `skills/` path. The maintained `bun scripts/probe-codex-mcp-runtime.ts --json` headless probe now also shows a more precise MCP approval split: default project-scoped and user-scoped root MCP both emit a real `mcp_tool_call` item but fail it with `user cancelled MCP tool call` before any server-side `tools/call`, while the default inline-agent path reaches startup plus `tools/list` and then falls back to `MCP_PROOF_MARKER_MISSING`. The same maintained suite now also proves five approved allow-paths: explicit `[mcp_servers.<id>.tools.<tool>] approval_mode = "approve"` works for project-scoped root MCP, user-scoped root MCP, agent-local inline `mcp_servers`, and custom agents that inherit an approved project-scoped or user-scoped root MCP server. All three approved custom-agent MCP paths still avoid a root `mcp_tool_call` item in the parent `codex exec --json` stream and instead surface child `agents_states` moving through `pending_init` to `completed`; project-scoped servers still do not appear in `codex mcp list`, and user-scoped servers do appear there. The maintained `bun scripts/probe-codex-agents-interactive-runtime.ts --json` trusted interactive probe also showed the same `sandbox_mode = "read-only"` child agent still wrote to the workspace there too, so these fields are not yet uniformly trustworthy runtime boundaries.'
8742
8742
  },
8743
8743
  hooks: {
8744
8744
  mode: "translate",
8745
8745
  nativeSurfaces: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
8746
- notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin and still tracks the project/user config paths plus the codex_hooks feature-gate caveat."
8746
+ notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin and still tracks the project/user config paths plus the mixed `[features].hooks` / `[features].codex_hooks` activation caveat and current runtime/doc drift."
8747
8747
  },
8748
8748
  permissions: {
8749
8749
  mode: "translate",
8750
8750
  nativeSurfaces: ["approvals", "sandbox policy", "hook matchers", "custom agent config"],
8751
- notes: "Codex expresses permission intent through approvals, sandboxing, hooks, and custom agents rather than skill frontmatter."
8751
+ notes: 'Codex expresses permission intent through approvals, sandboxing, hooks, and custom agents rather than skill frontmatter. Pluxx now also emits `.codex/config.generated.toml` for the live-proven top-level MCP allow-path when canonical `MCP(...)` rules are concrete enough to materialize per-tool `approval_mode = "approve"` stanzas, while `.codex/permissions.generated.json` remains the broader advisory mirror.'
8752
8752
  },
8753
8753
  runtime: {
8754
8754
  mode: "preserve",
8755
8755
  nativeSurfaces: [".mcp.json", ".app.json", ".codex/config.toml", "scripts/", "assets/"],
8756
- notes: `Bundle-local MCP config exists, but active MCP state also lives in config.toml. ${getRuntimeReadinessExternalConfigNote()}`
8756
+ notes: `Bundle-local MCP config exists, but active MCP state also lives in config.toml. Local May 13, 2026 headless Codex MCP probes reached startup plus \`tools/list\` across project-scoped, user-scoped, and inline-agent config; default root MCP emitted a real \`mcp_tool_call\` item but failed it with \`user cancelled MCP tool call\` before any server-side \`tools/call\`, while the default inline-agent path fell back to \`MCP_PROOF_MARKER_MISSING\` after startup plus \`tools/list\`. The same maintained suite now proves five concrete approval paths: project-scoped root MCP, user-scoped root MCP, agent-local inline \`mcp_servers\`, a custom agent inheriting an approved project-scoped root MCP server, and a custom agent inheriting an approved user-scoped root MCP server all reach real server-side \`tools/call\` once explicit \`[mcp_servers.<id>.tools.<tool>] approval_mode = "approve"\` is present in the relevant layer. All three approved custom-agent MCP paths still avoid a root \`mcp_tool_call\` item in the parent \`codex exec --json\` stream and instead surface child \`agents_states\` moving through \`pending_init\` to \`completed\`; project-scoped servers still do not appear in \`codex mcp list\`, and user-scoped servers do appear there. ${getRuntimeReadinessExternalConfigNote()}`
8757
8757
  },
8758
8758
  distribution: {
8759
8759
  mode: "preserve",
@@ -10294,12 +10294,13 @@ function runPublish(config, options = {}) {
10294
10294
  }
10295
10295
 
10296
10296
  // src/cli/verify-install.ts
10297
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync, statSync } from "fs";
10297
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, statSync } from "fs";
10298
10298
  import { resolve as resolve5 } from "path";
10299
10299
 
10300
10300
  // src/cli/doctor.ts
10301
10301
  import { spawn, spawnSync as spawnSync2 } from "child_process";
10302
- import { accessSync, constants, existsSync as existsSync4, lstatSync, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
10302
+ import { accessSync, constants, existsSync as existsSync4, lstatSync, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "fs";
10303
+ import { homedir } from "os";
10303
10304
  import { basename, dirname as dirname2, resolve as resolve4 } from "path";
10304
10305
 
10305
10306
  // node_modules/jiti/lib/jiti.mjs
@@ -10466,7 +10467,7 @@ function resolveBundleReference(rootDir, value) {
10466
10467
  }
10467
10468
  function readBundleManifestReferences(manifest) {
10468
10469
  const references = [];
10469
- for (const key of ["commands", "skills", "hooks", "mcpServers"]) {
10470
+ for (const key of ["commands", "skills", "hooks", "mcpServers", "rules", "apps"]) {
10470
10471
  const value = manifest[key];
10471
10472
  if (typeof value === "string") {
10472
10473
  references.push(value);
@@ -10504,6 +10505,37 @@ function extractBundleCommandTargets(command) {
10504
10505
  const matches = command.match(/\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/][^\s"'`;$|&<>]+|\.\.?[\\/][^\s"'`;$|&<>]+/g);
10505
10506
  return matches ?? [];
10506
10507
  }
10508
+ function resolveInstalledHooksReference(rootDir, platform, manifest) {
10509
+ const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
10510
+ if (manifestReference) {
10511
+ return {
10512
+ reference: manifestReference,
10513
+ path: resolveBundleReference(rootDir, manifestReference)
10514
+ };
10515
+ }
10516
+ if (platform === "claude-code") {
10517
+ const fallbackReference = "./hooks/hooks.json";
10518
+ const fallbackPath = resolve3(rootDir, "hooks/hooks.json");
10519
+ if (existsSync3(fallbackPath)) {
10520
+ return {
10521
+ reference: fallbackReference,
10522
+ path: fallbackPath
10523
+ };
10524
+ }
10525
+ }
10526
+ return {};
10527
+ }
10528
+ function getClaudeStandardHooksManifestIssue(rootDir, platform, manifest) {
10529
+ if (platform !== "claude-code") return void 0;
10530
+ const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
10531
+ if (!manifestReference) return void 0;
10532
+ const manifestHooksPath = resolveBundleReference(rootDir, manifestReference);
10533
+ const standardHooksPath = resolve3(rootDir, "hooks/hooks.json");
10534
+ if (!manifestHooksPath || manifestHooksPath !== standardHooksPath || !existsSync3(standardHooksPath)) {
10535
+ return void 0;
10536
+ }
10537
+ return "Claude auto-loads hooks/hooks.json. Current Claude CLI releases report a duplicate hooks file load error when manifest.hooks also points at ./hooks/hooks.json, so manifest.hooks should only reference additional hook files.";
10538
+ }
10507
10539
  function findInstalledBundleIntegrityIssues(rootDir, platform) {
10508
10540
  const manifestPath = manifestPathForPlatform(platform);
10509
10541
  if (!manifestPath) {
@@ -10537,17 +10569,19 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10537
10569
  const resolved = resolveBundleReference(rootDir, value);
10538
10570
  return resolved !== void 0 && !existsSync3(resolved);
10539
10571
  }).sort();
10540
- const hooksReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
10572
+ const manifestIssue = getClaudeStandardHooksManifestIssue(rootDir, platform, manifest);
10573
+ const { reference: hooksReference, path: hooksPath } = resolveInstalledHooksReference(rootDir, platform, manifest);
10541
10574
  if (!hooksReference) {
10542
10575
  return {
10576
+ ...manifestIssue ? { manifestIssue } : {},
10543
10577
  missingManifestPaths,
10544
10578
  missingHookTargets: [],
10545
10579
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
10546
10580
  };
10547
10581
  }
10548
- const hooksPath = resolveBundleReference(rootDir, hooksReference);
10549
10582
  if (!hooksPath || !existsSync3(hooksPath)) {
10550
10583
  return {
10584
+ ...manifestIssue ? { manifestIssue } : {},
10551
10585
  missingManifestPaths,
10552
10586
  missingHookTargets: [],
10553
10587
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
@@ -10564,12 +10598,15 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10564
10598
  })
10565
10599
  )].sort();
10566
10600
  return {
10601
+ ...manifestIssue ? { manifestIssue } : {},
10567
10602
  missingManifestPaths,
10568
10603
  missingHookTargets,
10569
10604
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
10570
10605
  };
10571
- } catch {
10606
+ } catch (error) {
10572
10607
  return {
10608
+ ...manifestIssue ? { manifestIssue } : {},
10609
+ hookConfigIssue: `hooks config at ${hooksReference} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
10573
10610
  missingManifestPaths,
10574
10611
  missingHookTargets: [],
10575
10612
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
@@ -10754,6 +10791,180 @@ var MUTATING_PREFIXES = [
10754
10791
  ];
10755
10792
  var MUTATING_PREFIX_PATTERN = new RegExp(`^(${MUTATING_PREFIXES.join("|")})\\b`, "i");
10756
10793
 
10794
+ // src/codex-hooks-feature.ts
10795
+ var RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
10796
+ var ALTERNATE_CODEX_HOOKS_FEATURE_FLAG = "codex_hooks";
10797
+ function getCodexHooksFeatureState(features) {
10798
+ if (!features) {
10799
+ return {
10800
+ recommended: false,
10801
+ alternate: false
10802
+ };
10803
+ }
10804
+ return {
10805
+ recommended: features[RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG] === true,
10806
+ alternate: features[ALTERNATE_CODEX_HOOKS_FEATURE_FLAG] === true
10807
+ };
10808
+ }
10809
+
10810
+ // src/toml-lite.ts
10811
+ function stripTomlComment(line) {
10812
+ let inString = false;
10813
+ let quote = "";
10814
+ for (let i = 0; i < line.length; i += 1) {
10815
+ const char = line[i];
10816
+ if ((char === '"' || char === "'") && line[i - 1] !== "\\") {
10817
+ if (!inString) {
10818
+ inString = true;
10819
+ quote = char;
10820
+ } else if (quote === char) {
10821
+ inString = false;
10822
+ quote = "";
10823
+ }
10824
+ continue;
10825
+ }
10826
+ if (char === "#" && !inString) return line.slice(0, i);
10827
+ }
10828
+ return line;
10829
+ }
10830
+ function parseTomlValue(value) {
10831
+ if (value.startsWith('"') && value.endsWith('"')) return unquoteTomlString(value);
10832
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
10833
+ if (value.startsWith("[") && value.endsWith("]")) {
10834
+ const inner = value.slice(1, -1).trim();
10835
+ if (!inner) return [];
10836
+ return splitTomlList(inner).map((part) => parseTomlValue(part.trim()));
10837
+ }
10838
+ if (value.startsWith("{") && value.endsWith("}")) {
10839
+ const inner = value.slice(1, -1).trim();
10840
+ const result = {};
10841
+ if (!inner) return result;
10842
+ for (const part of splitTomlList(inner)) {
10843
+ const assignment = part.trim().match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
10844
+ if (!assignment) continue;
10845
+ result[assignment[1]] = parseTomlValue(assignment[2].trim());
10846
+ }
10847
+ return result;
10848
+ }
10849
+ if (value === "true") return true;
10850
+ if (value === "false") return false;
10851
+ return value;
10852
+ }
10853
+ function splitTomlList(value) {
10854
+ const parts = [];
10855
+ let current = "";
10856
+ let inString = false;
10857
+ let quote = "";
10858
+ let braceDepth = 0;
10859
+ for (let i = 0; i < value.length; i += 1) {
10860
+ const char = value[i];
10861
+ if ((char === '"' || char === "'") && value[i - 1] !== "\\") {
10862
+ if (!inString) {
10863
+ inString = true;
10864
+ quote = char;
10865
+ } else if (quote === char) {
10866
+ inString = false;
10867
+ quote = "";
10868
+ }
10869
+ }
10870
+ if (!inString && char === "{") braceDepth += 1;
10871
+ if (!inString && char === "}") braceDepth -= 1;
10872
+ if (!inString && braceDepth === 0 && char === ",") {
10873
+ parts.push(current);
10874
+ current = "";
10875
+ continue;
10876
+ }
10877
+ current += char;
10878
+ }
10879
+ if (current.trim()) parts.push(current);
10880
+ return parts;
10881
+ }
10882
+ function unquoteTomlString(value) {
10883
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
10884
+ }
10885
+
10886
+ // src/codex-permissions-companion.ts
10887
+ function parseCodexApprovedMcpToolsFromToml(source) {
10888
+ const approvals = /* @__PURE__ */ new Map();
10889
+ let currentSection = null;
10890
+ const addApproval = (entry) => {
10891
+ approvals.set(`${entry.serverName}.${entry.toolName}`, entry);
10892
+ };
10893
+ for (const rawLine of source.split(/\r?\n/)) {
10894
+ const line = stripTomlComment(rawLine).trim();
10895
+ if (!line) continue;
10896
+ const sectionMatch = line.match(/^\[(.+)\]$/);
10897
+ if (sectionMatch) {
10898
+ currentSection = parseCodexApprovalSection(sectionMatch[1].trim());
10899
+ continue;
10900
+ }
10901
+ const assignmentMatch = line.match(/^([^=]+?)\s*=\s*(.+)$/);
10902
+ if (!assignmentMatch) continue;
10903
+ const key = assignmentMatch[1].trim();
10904
+ const parsedValue = parseTomlValue(assignmentMatch[2].trim());
10905
+ if (currentSection && key === "approval_mode" && parsedValue === "approve") {
10906
+ addApproval(currentSection);
10907
+ continue;
10908
+ }
10909
+ if (!key.includes(".")) continue;
10910
+ const entry = parseCodexApprovalAssignment(key, parsedValue);
10911
+ if (entry) addApproval(entry);
10912
+ }
10913
+ return [...approvals.values()].sort((a, b) => a.serverName.localeCompare(b.serverName) || a.toolName.localeCompare(b.toolName));
10914
+ }
10915
+ function parseCodexApprovalSection(sectionName) {
10916
+ const tokens = splitTomlDottedPath(sectionName);
10917
+ if (tokens.length !== 4) return null;
10918
+ if (tokens[0] !== "mcp_servers" || tokens[2] !== "tools") return null;
10919
+ return {
10920
+ serverName: tokens[1],
10921
+ toolName: tokens[3]
10922
+ };
10923
+ }
10924
+ function parseCodexApprovalAssignment(key, value) {
10925
+ if (value !== "approve") return null;
10926
+ const tokens = splitTomlDottedPath(key);
10927
+ if (tokens.length !== 5) return null;
10928
+ if (tokens[0] !== "mcp_servers" || tokens[2] !== "tools" || tokens[4] !== "approval_mode") return null;
10929
+ return {
10930
+ serverName: tokens[1],
10931
+ toolName: tokens[3]
10932
+ };
10933
+ }
10934
+ function splitTomlDottedPath(value) {
10935
+ const parts = [];
10936
+ let current = "";
10937
+ let inString = false;
10938
+ let quote = "";
10939
+ for (let i = 0; i < value.length; i += 1) {
10940
+ const char = value[i];
10941
+ if ((char === '"' || char === "'") && value[i - 1] !== "\\") {
10942
+ if (!inString) {
10943
+ inString = true;
10944
+ quote = char;
10945
+ } else if (quote === char) {
10946
+ inString = false;
10947
+ quote = "";
10948
+ }
10949
+ current += char;
10950
+ continue;
10951
+ }
10952
+ if (!inString && char === ".") {
10953
+ const trimmed2 = current.trim();
10954
+ if (trimmed2) parts.push(trimmed2);
10955
+ current = "";
10956
+ continue;
10957
+ }
10958
+ current += char;
10959
+ }
10960
+ const trimmed = current.trim();
10961
+ if (trimmed) parts.push(trimmed);
10962
+ return parts.map((part) => {
10963
+ const parsed = parseTomlValue(part);
10964
+ return typeof parsed === "string" ? parsed : part;
10965
+ });
10966
+ }
10967
+
10757
10968
  // src/cli/doctor.ts
10758
10969
  var MATERIALIZED_ENV_MARKER = "materialized required config";
10759
10970
  var MIN_NODE_MAJOR = 18;
@@ -10860,6 +11071,278 @@ function detectConsumerLayout(rootDir) {
10860
11071
  function readJsonFile(rootDir, relativePath) {
10861
11072
  return JSON.parse(readFileSync4(resolve4(rootDir, relativePath), "utf-8"));
10862
11073
  }
11074
+ function listCodexConfigCandidates(projectRoot) {
11075
+ const projectCandidate = resolve4(projectRoot ?? process.cwd(), ".codex/config.toml");
11076
+ const homeDir = process.env.HOME?.trim() || homedir();
11077
+ const codexHome = process.env.CODEX_HOME?.trim() || resolve4(homeDir, ".codex");
11078
+ const userCandidate = resolve4(codexHome, "config.toml");
11079
+ const seen = /* @__PURE__ */ new Set();
11080
+ const candidates = [];
11081
+ for (const candidate of [
11082
+ { path: projectCandidate, scope: "project" },
11083
+ { path: userCandidate, scope: "user" }
11084
+ ]) {
11085
+ if (seen.has(candidate.path)) continue;
11086
+ seen.add(candidate.path);
11087
+ candidates.push(candidate);
11088
+ }
11089
+ return candidates;
11090
+ }
11091
+ function readCodexHooksFeatureFlag(filePath) {
11092
+ let inFeaturesTable = false;
11093
+ const lines = readFileSync4(filePath, "utf-8").split(/\r?\n/);
11094
+ let recommended;
11095
+ let alternate;
11096
+ const assignFeatureFlag = (key, rawValue) => {
11097
+ const parsed = parseTomlValue(rawValue.trim());
11098
+ if (typeof parsed !== "boolean") return;
11099
+ if (key === RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG) recommended = parsed;
11100
+ if (key === ALTERNATE_CODEX_HOOKS_FEATURE_FLAG) alternate = parsed;
11101
+ };
11102
+ for (const rawLine of lines) {
11103
+ const line = stripTomlComment(rawLine).trim();
11104
+ if (!line) continue;
11105
+ const sectionMatch = line.match(/^\[(.+)\]$/);
11106
+ if (sectionMatch) {
11107
+ inFeaturesTable = sectionMatch[1].trim() === "features";
11108
+ continue;
11109
+ }
11110
+ const dottedFeatureMatch = line.match(/^features\.(hooks|codex_hooks)\s*=\s*(.+)$/);
11111
+ if (dottedFeatureMatch) {
11112
+ assignFeatureFlag(dottedFeatureMatch[1], dottedFeatureMatch[2]);
11113
+ continue;
11114
+ }
11115
+ const inlineFeaturesMatch = line.match(/^features\s*=\s*(.+)$/);
11116
+ if (inlineFeaturesMatch) {
11117
+ const parsed = parseTomlValue(inlineFeaturesMatch[1].trim());
11118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11119
+ const featureState = getCodexHooksFeatureState(parsed);
11120
+ recommended = featureState.recommended;
11121
+ alternate = featureState.alternate;
11122
+ }
11123
+ continue;
11124
+ }
11125
+ if (!inFeaturesTable) continue;
11126
+ const featureMatch = line.match(/^(hooks|codex_hooks)\s*=\s*(.+)$/);
11127
+ if (!featureMatch) continue;
11128
+ assignFeatureFlag(featureMatch[1], featureMatch[2]);
11129
+ }
11130
+ if (recommended === void 0 && alternate === void 0) return void 0;
11131
+ return {
11132
+ recommended: recommended === true,
11133
+ alternate: alternate === true
11134
+ };
11135
+ }
11136
+ function probeCodexHooksFeatureFlags(projectRoot) {
11137
+ return listCodexConfigCandidates(projectRoot).map((candidate) => {
11138
+ if (!existsSync4(candidate.path)) {
11139
+ return {
11140
+ ...candidate,
11141
+ exists: false,
11142
+ enabled: false,
11143
+ recommendedEnabled: false,
11144
+ alternateEnabled: false
11145
+ };
11146
+ }
11147
+ try {
11148
+ const featureState = readCodexHooksFeatureFlag(candidate.path);
11149
+ return {
11150
+ ...candidate,
11151
+ exists: true,
11152
+ enabled: featureState?.recommended === true || featureState?.alternate === true,
11153
+ recommendedEnabled: featureState?.recommended === true,
11154
+ alternateEnabled: featureState?.alternate === true
11155
+ };
11156
+ } catch (error) {
11157
+ return {
11158
+ ...candidate,
11159
+ exists: true,
11160
+ enabled: false,
11161
+ recommendedEnabled: false,
11162
+ alternateEnabled: false,
11163
+ parseError: error instanceof Error ? error.message : String(error)
11164
+ };
11165
+ }
11166
+ });
11167
+ }
11168
+ function probeCodexMcpApprovalEntries(projectRoot) {
11169
+ return listCodexConfigCandidates(projectRoot).map((candidate) => {
11170
+ if (!existsSync4(candidate.path)) {
11171
+ return {
11172
+ ...candidate,
11173
+ exists: false,
11174
+ approvals: []
11175
+ };
11176
+ }
11177
+ try {
11178
+ return {
11179
+ ...candidate,
11180
+ exists: true,
11181
+ approvals: parseCodexApprovedMcpToolsFromToml(readFileSync4(candidate.path, "utf-8"))
11182
+ };
11183
+ } catch (error) {
11184
+ return {
11185
+ ...candidate,
11186
+ exists: true,
11187
+ approvals: [],
11188
+ parseError: error instanceof Error ? error.message : String(error)
11189
+ };
11190
+ }
11191
+ });
11192
+ }
11193
+ function getCodexProjectPathCandidates(projectRoot) {
11194
+ if (!projectRoot) return [];
11195
+ const candidates = /* @__PURE__ */ new Set();
11196
+ const resolvedProjectRoot = resolve4(projectRoot);
11197
+ candidates.add(resolvedProjectRoot);
11198
+ try {
11199
+ candidates.add(realpathSync(resolvedProjectRoot));
11200
+ } catch {
11201
+ }
11202
+ return [...candidates];
11203
+ }
11204
+ function parseCodexProjectKeySegment(segment) {
11205
+ const trimmed = segment.trim();
11206
+ if (!trimmed) return null;
11207
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
11208
+ const parsed = parseTomlValue(trimmed);
11209
+ return typeof parsed === "string" ? parsed : null;
11210
+ }
11211
+ return null;
11212
+ }
11213
+ function readCodexProjectTrust(filePath, projectPaths) {
11214
+ const matchedPaths = /* @__PURE__ */ new Set();
11215
+ let currentProjectKey = null;
11216
+ const lines = readFileSync4(filePath, "utf-8").split(/\r?\n/);
11217
+ for (const rawLine of lines) {
11218
+ const line = stripTomlComment(rawLine).trim();
11219
+ if (!line) continue;
11220
+ const sectionMatch = line.match(/^\[(.+)\]$/);
11221
+ if (sectionMatch) {
11222
+ currentProjectKey = null;
11223
+ const section = sectionMatch[1].trim();
11224
+ if (section.startsWith("projects.")) {
11225
+ currentProjectKey = parseCodexProjectKeySegment(section.slice("projects.".length));
11226
+ }
11227
+ continue;
11228
+ }
11229
+ const dottedTrustMatch = line.match(/^projects\.(.+)\.trust_level\s*=\s*(.+)$/);
11230
+ if (dottedTrustMatch) {
11231
+ const projectKey = parseCodexProjectKeySegment(dottedTrustMatch[1]);
11232
+ const parsed2 = parseTomlValue(dottedTrustMatch[2].trim());
11233
+ if (projectKey && projectPaths.has(projectKey) && parsed2 === "trusted") {
11234
+ matchedPaths.add(projectKey);
11235
+ }
11236
+ continue;
11237
+ }
11238
+ if (!currentProjectKey || !projectPaths.has(currentProjectKey)) continue;
11239
+ const trustMatch = line.match(/^trust_level\s*=\s*(.+)$/);
11240
+ if (!trustMatch) continue;
11241
+ const parsed = parseTomlValue(trustMatch[1].trim());
11242
+ if (parsed === "trusted") {
11243
+ matchedPaths.add(currentProjectKey);
11244
+ }
11245
+ }
11246
+ return [...matchedPaths];
11247
+ }
11248
+ function probeCodexProjectTrust(projectRoot) {
11249
+ const projectPaths = getCodexProjectPathCandidates(projectRoot);
11250
+ if (projectPaths.length === 0) return void 0;
11251
+ const userConfig = listCodexConfigCandidates(projectRoot).find((candidate) => candidate.scope === "user");
11252
+ if (!userConfig) return void 0;
11253
+ if (!existsSync4(userConfig.path)) {
11254
+ return {
11255
+ path: userConfig.path,
11256
+ exists: false,
11257
+ trusted: false,
11258
+ matchedProjectPaths: []
11259
+ };
11260
+ }
11261
+ try {
11262
+ const matchedProjectPaths = readCodexProjectTrust(userConfig.path, new Set(projectPaths));
11263
+ return {
11264
+ path: userConfig.path,
11265
+ exists: true,
11266
+ trusted: matchedProjectPaths.length > 0,
11267
+ matchedProjectPaths
11268
+ };
11269
+ } catch (error) {
11270
+ return {
11271
+ path: userConfig.path,
11272
+ exists: true,
11273
+ trusted: false,
11274
+ matchedProjectPaths: [],
11275
+ parseError: error instanceof Error ? error.message : String(error)
11276
+ };
11277
+ }
11278
+ }
11279
+ function getClaudeManagedSettingsPath() {
11280
+ switch (process.platform) {
11281
+ case "darwin":
11282
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
11283
+ case "linux":
11284
+ return "/etc/claude-code/managed-settings.json";
11285
+ case "win32":
11286
+ return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
11287
+ default:
11288
+ return void 0;
11289
+ }
11290
+ }
11291
+ function listClaudeSettingsCandidates(projectRoot) {
11292
+ const homeDir = process.env.HOME?.trim() || homedir();
11293
+ const candidates = [];
11294
+ const managedPath = getClaudeManagedSettingsPath();
11295
+ if (managedPath) {
11296
+ candidates.push({ path: managedPath, scope: "managed" });
11297
+ }
11298
+ candidates.push({ path: resolve4(homeDir, ".claude/settings.json"), scope: "user" });
11299
+ if (projectRoot) {
11300
+ candidates.push({ path: resolve4(projectRoot, ".claude/settings.json"), scope: "project" });
11301
+ candidates.push({ path: resolve4(projectRoot, ".claude/settings.local.json"), scope: "local" });
11302
+ }
11303
+ const seen = /* @__PURE__ */ new Set();
11304
+ return candidates.filter((candidate) => {
11305
+ if (seen.has(candidate.path)) return false;
11306
+ seen.add(candidate.path);
11307
+ return true;
11308
+ });
11309
+ }
11310
+ function readClaudeDisableAllHooks(filePath) {
11311
+ const parsed = readJsonFile(dirname2(filePath), basename(filePath));
11312
+ return typeof parsed.disableAllHooks === "boolean" ? parsed.disableAllHooks : void 0;
11313
+ }
11314
+ function probeClaudeDisableAllHooks(projectRoot) {
11315
+ return listClaudeSettingsCandidates(projectRoot).map((candidate) => {
11316
+ if (!existsSync4(candidate.path)) {
11317
+ return {
11318
+ ...candidate,
11319
+ exists: false,
11320
+ disableAllHooks: false
11321
+ };
11322
+ }
11323
+ try {
11324
+ return {
11325
+ ...candidate,
11326
+ exists: true,
11327
+ disableAllHooks: readClaudeDisableAllHooks(candidate.path) === true
11328
+ };
11329
+ } catch (error) {
11330
+ return {
11331
+ ...candidate,
11332
+ exists: true,
11333
+ disableAllHooks: false,
11334
+ parseError: error instanceof Error ? error.message : String(error)
11335
+ };
11336
+ }
11337
+ });
11338
+ }
11339
+ function getInstalledClaudeHooksReference(rootDir, manifest) {
11340
+ if (existsSync4(resolve4(rootDir, "hooks/hooks.json"))) {
11341
+ return "./hooks/hooks.json";
11342
+ }
11343
+ const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
11344
+ return manifestReference;
11345
+ }
10863
11346
  function checkConsumerBundlePath(checks, rootDir) {
10864
11347
  try {
10865
11348
  accessSync(rootDir, constants.R_OK);
@@ -11159,6 +11642,9 @@ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
11159
11642
  if (issues.manifestIssue) {
11160
11643
  details.push(issues.manifestIssue);
11161
11644
  }
11645
+ if (issues.hookConfigIssue) {
11646
+ details.push(issues.hookConfigIssue);
11647
+ }
11162
11648
  if (issues.missingManifestPaths.length > 0) {
11163
11649
  details.push(`manifest references missing path${issues.missingManifestPaths.length === 1 ? "" : "s"}: ${issues.missingManifestPaths.join(", ")}`);
11164
11650
  }
@@ -11182,12 +11668,207 @@ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
11182
11668
  addCheck(checks, {
11183
11669
  level: "error",
11184
11670
  code: "consumer-bundle-integrity-invalid",
11185
- title: "Installed bundle is missing referenced files",
11671
+ title: "Installed bundle integrity is broken",
11186
11672
  detail: details.join("; "),
11187
- fix: "Reinstall the plugin or rebuild the bundle so every manifest path and hook target exists in the installed plugin.",
11673
+ fix: "Reinstall the plugin or rebuild the bundle so every manifest path, hook config, and hook target is valid inside the installed plugin.",
11188
11674
  path: layout.manifestPath
11189
11675
  });
11190
11676
  }
11677
+ function checkInstalledClaudeHookSettings(checks, rootDir, layout, options) {
11678
+ if (layout.platform !== "claude-code") return;
11679
+ if (checks.some((check) => check.code === "consumer-bundle-integrity-invalid")) return;
11680
+ let manifest;
11681
+ try {
11682
+ manifest = readJsonFile(rootDir, layout.manifestPath);
11683
+ } catch {
11684
+ return;
11685
+ }
11686
+ const hooksReference = getInstalledClaudeHooksReference(rootDir, manifest);
11687
+ if (!hooksReference) return;
11688
+ const probes = probeClaudeDisableAllHooks(options.projectRoot);
11689
+ const disabled = probes.filter((probe) => probe.disableAllHooks);
11690
+ const parseErrors = probes.filter((probe) => probe.parseError).map((probe) => `${probe.path}: ${probe.parseError}`);
11691
+ const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "settings" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
11692
+ if (disabled.length > 0) {
11693
+ addCheck(checks, {
11694
+ level: "warning",
11695
+ code: "consumer-claude-hooks-disabled",
11696
+ title: "Claude settings may currently suppress bundled hook activation",
11697
+ detail: `This installed Claude bundle uses hooks at ${hooksReference}, and \`disableAllHooks = true\` was found in ${disabled.map((probe) => `${probe.scope} settings ${probe.path}`).join(" and ")}. Live Claude CLI 2.1.140 headless probes on 2026-05-13 showed this setting suppressing SessionStart settings-hook execution across user, project, and local layers.${parseErrors.length > 0 ? ` Unparseable settings: ${parseErrors.join("; ")}.` : ""} This check only inspects the file-based managed path plus user/project/local settings; Claude enterprise policy can also arrive through registry, plist/MDM, or server-managed policy, and \`allowManagedHooksOnly\` is not evaluated here.`,
11698
+ fix: "Remove or flip `disableAllHooks` in the active Claude settings layer, run /reload-plugins or restart Claude, rerun pluxx verify-install, and use Claude /status when enterprise managed policy may still control hook activation.",
11699
+ path: disabled[0].path
11700
+ });
11701
+ return;
11702
+ }
11703
+ if (parseErrors.length > 0) {
11704
+ addCheck(checks, {
11705
+ level: "warning",
11706
+ code: "consumer-claude-hook-settings-invalid",
11707
+ title: "Claude hook settings could not be fully inspected",
11708
+ detail: `This installed Claude bundle uses hooks at ${hooksReference}, but some checked Claude settings files were not parseable. Checked ${checkedPaths}. Unparseable settings: ${parseErrors.join("; ")}. This check only inspects the file-based managed path plus user/project/local settings; it does not verify registry, plist/MDM, server-managed policy, or \`allowManagedHooksOnly\`.`,
11709
+ fix: "Fix the malformed Claude settings JSON, reload Claude, rerun pluxx verify-install, and use Claude /status if enterprise policy may still control hook activation.",
11710
+ path: probes.find((probe) => probe.parseError)?.path ?? hooksReference
11711
+ });
11712
+ return;
11713
+ }
11714
+ addCheck(checks, {
11715
+ level: "success",
11716
+ code: "consumer-claude-hook-settings-clear",
11717
+ title: "No checked Claude settings layer is disabling hooks",
11718
+ detail: `This installed Claude bundle uses hooks at ${hooksReference}, and no checked Claude settings layer currently sets \`disableAllHooks = true\`. Checked ${checkedPaths}. This success result only covers the file-based managed path plus user/project/local settings; it does not prove registry, plist/MDM, server-managed policy, or \`allowManagedHooksOnly\` are clear.`,
11719
+ fix: "No action needed.",
11720
+ path: hooksReference
11721
+ });
11722
+ }
11723
+ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
11724
+ if (layout.platform !== "codex") return;
11725
+ if (checks.some((check) => check.code === "consumer-bundle-integrity-invalid")) return;
11726
+ let manifest;
11727
+ try {
11728
+ manifest = readJsonFile(rootDir, layout.manifestPath);
11729
+ } catch {
11730
+ return;
11731
+ }
11732
+ const hooksReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
11733
+ if (!hooksReference) return;
11734
+ const probes = probeCodexHooksFeatureFlags(options.projectRoot);
11735
+ const enabledProbes = probes.filter((probe) => probe.enabled);
11736
+ const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "config" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
11737
+ const describeEnabledFlags = (probe) => {
11738
+ const enabledFlags = [];
11739
+ if (probe.recommendedEnabled) enabledFlags.push(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
11740
+ if (probe.alternateEnabled) enabledFlags.push(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
11741
+ return enabledFlags.join(" + ");
11742
+ };
11743
+ if (enabledProbes.length > 0) {
11744
+ const legacyOnlyProbes = enabledProbes.filter((probe) => probe.alternateEnabled && !probe.recommendedEnabled);
11745
+ if (legacyOnlyProbes.length > 0) {
11746
+ addCheck(checks, {
11747
+ level: "warning",
11748
+ code: "consumer-codex-hooks-feature-flag-legacy-only",
11749
+ title: "Codex hooks are enabled only through the legacy compatibility flag",
11750
+ detail: `This installed Codex bundle declares hooks at ${hooksReference}, and the checked Codex config enables only \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. Maintained interactive probes on May 13, 2026 showed local Codex CLI 0.130.0 emitting a deprecation warning for \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` that points users to \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\`.`,
11751
+ fix: `Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, keep \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` only as a compatibility fallback if needed, reload Codex, and rerun pluxx verify-install.`,
11752
+ path: legacyOnlyProbes[0].path
11753
+ });
11754
+ }
11755
+ addCheck(checks, {
11756
+ level: "success",
11757
+ code: "consumer-codex-hooks-feature-flag-enabled",
11758
+ title: "Codex hook feature flag found for this install",
11759
+ detail: `This installed Codex bundle declares hooks at ${hooksReference}, and a Codex hook feature flag was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution: maintained local probes on May 13, 2026 showed local Codex CLI 0.130.0 still failing to execute the project-local hook under \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\`, \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\`, and the current CLI feature path \`--enable hooks\`.`,
11760
+ fix: "No action needed.",
11761
+ path: enabledProbes[0].path
11762
+ });
11763
+ return;
11764
+ }
11765
+ const parseErrors = probes.filter((probe) => probe.parseError).map((probe) => `${probe.path}: ${probe.parseError}`);
11766
+ addCheck(checks, {
11767
+ level: "warning",
11768
+ code: "consumer-codex-hooks-feature-flag-missing",
11769
+ title: "Codex hook activation is missing its known feature-gate prerequisite",
11770
+ detail: `This installed Codex bundle declares hooks at ${hooksReference}, but neither \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` nor \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` was found in the checked Codex config layers. Current Codex config surfaces still accept both keys under \`[features]\`, but maintained probes on May 13, 2026 showed local Codex CLI 0.130.0 still failing to execute the project-local hook under \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\`, \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\`, and the current CLI feature path \`--enable hooks\`, while also emitting a deprecation warning for \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` that points users to \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\`. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
11771
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install. Keep \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` only as a compatibility fallback if your Codex runtime still needs it, and treat any enabled flag as best-effort activation rather than proof that hooks will execute.`,
11772
+ path: probes.find((probe) => probe.exists)?.path ?? hooksReference
11773
+ });
11774
+ }
11775
+ function checkInstalledCodexProjectTrust(checks, rootDir, layout, options) {
11776
+ if (layout.platform !== "codex") return;
11777
+ if (checks.some((check) => check.code === "consumer-bundle-integrity-invalid")) return;
11778
+ if (!options.projectRoot) return;
11779
+ let manifest;
11780
+ try {
11781
+ manifest = readJsonFile(rootDir, layout.manifestPath);
11782
+ } catch {
11783
+ return;
11784
+ }
11785
+ const hooksReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
11786
+ if (!hooksReference) return;
11787
+ const trustProbe = probeCodexProjectTrust(options.projectRoot);
11788
+ if (!trustProbe) return;
11789
+ const projectPaths = getCodexProjectPathCandidates(options.projectRoot);
11790
+ const displayProjectPaths = projectPaths.join(" or ");
11791
+ const parseErrorDetail = trustProbe.parseError ? ` Unparseable user config: ${trustProbe.parseError}.` : "";
11792
+ if (trustProbe.trusted) {
11793
+ addCheck(checks, {
11794
+ level: "success",
11795
+ code: "consumer-codex-project-trust-enabled",
11796
+ title: "Codex trusted-project entry found for this install",
11797
+ detail: `This installed Codex bundle declares hooks at ${hooksReference}, and the user Codex config ${trustProbe.path} trusts ${trustProbe.matchedProjectPaths.join(" and ")} for project-local hook loading.`,
11798
+ fix: "No action needed.",
11799
+ path: trustProbe.path
11800
+ });
11801
+ return;
11802
+ }
11803
+ addCheck(checks, {
11804
+ level: "warning",
11805
+ code: "consumer-codex-project-trust-missing",
11806
+ title: "Codex project trust may still block hook activation",
11807
+ detail: `This installed Codex bundle declares hooks at ${hooksReference}, but the user Codex config ${trustProbe.exists ? trustProbe.path : `${trustProbe.path} (missing)`} does not currently trust ${displayProjectPaths}. Current Codex CLI releases can keep project-local config, hooks, and exec policies disabled until the project is trusted.${parseErrorDetail}`,
11808
+ fix: `Trust the project in Codex, or add a matching \`[projects."<absolute-project-path>"]\\ntrust_level = "trusted"\` entry in ${trustProbe.path}, reload Codex, retry a trusted interactive prompt, and rerun pluxx verify-install if hooks still do not activate.`,
11809
+ path: trustProbe.path
11810
+ });
11811
+ }
11812
+ function checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options) {
11813
+ if (layout.platform !== "codex") return;
11814
+ if (checks.some((check) => check.code === "consumer-bundle-integrity-invalid")) return;
11815
+ const companionPath = resolve4(rootDir, ".codex/config.generated.toml");
11816
+ const companionReference = ".codex/config.generated.toml";
11817
+ if (!existsSync4(companionPath)) return;
11818
+ let expectedEntries;
11819
+ try {
11820
+ expectedEntries = parseCodexApprovedMcpToolsFromToml(readFileSync4(companionPath, "utf-8"));
11821
+ } catch (error) {
11822
+ addCheck(checks, {
11823
+ level: "error",
11824
+ code: "consumer-codex-mcp-approval-companion-invalid",
11825
+ title: "Generated Codex MCP approval companion is malformed",
11826
+ detail: `${companionReference} could not be parsed: ${error instanceof Error ? error.message : String(error)}`,
11827
+ fix: "Rebuild or reinstall the plugin so the generated Codex MCP approval companion is written correctly before you merge it into active Codex config.",
11828
+ path: companionReference
11829
+ });
11830
+ return;
11831
+ }
11832
+ if (expectedEntries.length === 0) return;
11833
+ const probes = probeCodexMcpApprovalEntries(options.projectRoot);
11834
+ const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "config" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
11835
+ const parseErrors = probes.filter((probe) => probe.parseError).map((probe) => `${probe.path}: ${probe.parseError}`);
11836
+ const activeEntries = new Set(
11837
+ probes.flatMap((probe) => probe.approvals.map((entry) => `${entry.serverName}.${entry.toolName}`))
11838
+ );
11839
+ const missingEntries = expectedEntries.filter((entry) => !activeEntries.has(`${entry.serverName}.${entry.toolName}`));
11840
+ const formatEntries = (entries) => entries.map((entry) => `${entry.serverName}.${entry.toolName}`).join(", ");
11841
+ if (missingEntries.length > 0) {
11842
+ addCheck(checks, {
11843
+ level: "warning",
11844
+ code: "consumer-codex-mcp-approval-config-missing",
11845
+ title: "Generated Codex MCP approval stanzas are not fully merged into active config",
11846
+ detail: `This installed Codex bundle includes ${companionReference} with ${expectedEntries.length} per-tool approval stanza${expectedEntries.length === 1 ? "" : "s"}, but the checked Codex config layers are still missing ${missingEntries.length === expectedEntries.length ? "all of them" : formatEntries(missingEntries)}. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""} Live maintained Codex MCP probes on May 13, 2026 only proved this approval path once explicit per-tool \`approval_mode = "approve"\` entries were merged into active config.`,
11847
+ fix: `Merge the missing stanza${missingEntries.length === 1 ? "" : "s"} from ${companionReference} into the active project or user Codex config.toml, reload Codex, and rerun pluxx verify-install. Keep .codex/permissions.generated.json as the broader mirror for selectors that still remain external.`,
11848
+ path: companionReference
11849
+ });
11850
+ return;
11851
+ }
11852
+ if (parseErrors.length > 0) {
11853
+ addCheck(checks, {
11854
+ level: "warning",
11855
+ code: "consumer-codex-mcp-approval-config-unparseable",
11856
+ title: "Codex MCP approval config could not be fully inspected",
11857
+ detail: `This installed Codex bundle includes ${companionReference}, and matching approval stanzas were found in the checked Codex config layers, but some config files were not parseable. Checked ${checkedPaths}. Unparseable config: ${parseErrors.join("; ")}.`,
11858
+ fix: "Fix the malformed Codex config, reload Codex, and rerun pluxx verify-install so Pluxx can confirm the generated approval stanzas are still merged correctly.",
11859
+ path: probes.find((probe) => probe.parseError)?.path ?? companionReference
11860
+ });
11861
+ return;
11862
+ }
11863
+ addCheck(checks, {
11864
+ level: "success",
11865
+ code: "consumer-codex-mcp-approval-config-merged",
11866
+ title: "Generated Codex MCP approval stanzas are present in active config",
11867
+ detail: `This installed Codex bundle includes ${companionReference} with ${expectedEntries.length} per-tool approval stanza${expectedEntries.length === 1 ? "" : "s"}, and matching entries were found in the checked Codex config layers. Checked ${checkedPaths}.`,
11868
+ fix: "No action needed.",
11869
+ path: companionReference
11870
+ });
11871
+ }
11191
11872
  function parseInstalledPermissionRules(scriptSource) {
11192
11873
  const match = scriptSource.match(/const RULES = (\[[\s\S]*?\]);\nconst ACTION_PRIORITY/);
11193
11874
  if (!match?.[1]) {
@@ -11552,7 +12233,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
11552
12233
  const missingDetail = missingSkills.length > 0 ? `missing exported skills: ${missingSkills.join(", ")}` : void 0;
11553
12234
  const malformedDetail = malformedSkills.length > 0 ? `malformed exported skills: ${malformedSkills.join(", ")}` : void 0;
11554
12235
  addCheck(checks, {
11555
- level: "warning",
12236
+ level: "error",
11556
12237
  code: "consumer-opencode-skill-sync-incomplete",
11557
12238
  title: "OpenCode exported skills are incomplete",
11558
12239
  detail: [missingDetail, malformedDetail].filter(Boolean).join("; "),
@@ -11570,7 +12251,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
11570
12251
  path: "skills"
11571
12252
  });
11572
12253
  }
11573
- async function doctorConsumer(rootDir = process.cwd()) {
12254
+ async function doctorConsumer(rootDir = process.cwd(), options = {}) {
11574
12255
  const checks = [];
11575
12256
  addRuntimeChecks(checks, "consumer");
11576
12257
  checkConsumerBundlePath(checks, rootDir);
@@ -11615,6 +12296,10 @@ async function doctorConsumer(rootDir = process.cwd()) {
11615
12296
  });
11616
12297
  checkConsumerManifest(checks, rootDir, layout);
11617
12298
  checkInstalledBundleIntegrity(checks, rootDir, layout);
12299
+ checkInstalledClaudeHookSettings(checks, rootDir, layout, options);
12300
+ checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options);
12301
+ checkInstalledCodexProjectTrust(checks, rootDir, layout, options);
12302
+ checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options);
11618
12303
  checkInstalledPermissionHook(checks, rootDir, layout);
11619
12304
  checkInstalledUserConfig(checks, rootDir);
11620
12305
  checkInstalledEnvValidation(checks, rootDir);
@@ -11632,6 +12317,7 @@ function buildCheckFromReport(target, pluginName, report) {
11632
12317
  const consumerPath = resolveInstalledConsumerPath(target, pluginName);
11633
12318
  const staleReason = target.built && existsSync5(consumerPath) ? detectStaleInstall(target, pluginName, consumerPath) : void 0;
11634
12319
  const stale = staleReason !== void 0;
12320
+ const issues = listVerifyInstallIssues(report.checks);
11635
12321
  return {
11636
12322
  platform: target.platform,
11637
12323
  installPath: consumerPath,
@@ -11643,9 +12329,22 @@ function buildCheckFromReport(target, pluginName, report) {
11643
12329
  ok: report.errors === 0 && !stale,
11644
12330
  errors: report.errors + (stale ? 1 : 0),
11645
12331
  warnings: report.warnings,
11646
- infos: report.infos
12332
+ infos: report.infos,
12333
+ issues
11647
12334
  };
11648
12335
  }
12336
+ function listVerifyInstallIssues(checks) {
12337
+ return checks.filter(
12338
+ (check) => !check.code.startsWith("primitive-") && check.level !== "success"
12339
+ ).map((check) => ({
12340
+ level: check.level,
12341
+ code: check.code,
12342
+ title: check.title,
12343
+ detail: check.detail,
12344
+ fix: check.fix,
12345
+ ...check.path ? { path: check.path } : {}
12346
+ }));
12347
+ }
11649
12348
  function manifestPathForPlatform2(platform) {
11650
12349
  switch (platform) {
11651
12350
  case "claude-code":
@@ -11708,8 +12407,8 @@ function detectStaleInstall(target, pluginName, consumerPath) {
11708
12407
  try {
11709
12408
  const details = lstatSync2(consumerPath);
11710
12409
  if (details.isSymbolicLink()) {
11711
- const installedRealPath = realpathSync(consumerPath);
11712
- const builtRealPath = realpathSync(target.sourceDir);
12410
+ const installedRealPath = realpathSync2(consumerPath);
12411
+ const builtRealPath = realpathSync2(target.sourceDir);
11713
12412
  if (installedRealPath !== builtRealPath) {
11714
12413
  return `installed symlink points to ${readlinkSync(consumerPath)}, not the current build at ${target.sourceDir}`;
11715
12414
  }
@@ -11736,7 +12435,7 @@ async function verifyInstall(config, options = {}) {
11736
12435
  const checks = await Promise.all(
11737
12436
  filteredPlan.map(async (target) => {
11738
12437
  const consumerPath = resolveInstalledConsumerPath(target, config.name);
11739
- const report = await doctorConsumer(consumerPath);
12438
+ const report = await doctorConsumer(consumerPath, { projectRoot: rootDir });
11740
12439
  return buildCheckFromReport(target, config.name, report);
11741
12440
  })
11742
12441
  );
@@ -11754,6 +12453,13 @@ function printVerifyInstallResult(result) {
11754
12453
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
11755
12454
  console.log(` install path: ${check.installPath}`);
11756
12455
  console.log(` built: ${check.built ? "yes" : "no"}; installed: ${check.installed ? "yes" : "no"}; errors: ${check.errors}; warnings: ${check.warnings}; infos: ${check.infos}`);
12456
+ for (const issue of check.issues) {
12457
+ const issuePrefix = issue.level.toUpperCase().padEnd(7, " ");
12458
+ const pathLabel = issue.path ? ` [${issue.path}]` : "";
12459
+ console.log(` ${issuePrefix} ${issue.code}${pathLabel} ${issue.title}`);
12460
+ console.log(` ${issue.detail}`);
12461
+ console.log(` Fix: ${issue.fix}`);
12462
+ }
11757
12463
  if (check.stale) {
11758
12464
  console.log(` stale install: ${check.staleReason}`);
11759
12465
  }