@codyswann/lisa 2.332.1 → 2.332.3

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 (60) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +3 -1
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
  8. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
  9. package/plugins/lisa-agy/plugin.json +1 -1
  10. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
  11. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
  18. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  43. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails-agy/plugin.json +1 -1
  48. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  53. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +148 -3
package/package.json CHANGED
@@ -115,7 +115,7 @@
115
115
  "brace-expansion": ">=5.0.9"
116
116
  },
117
117
  "name": "@codyswann/lisa",
118
- "version": "2.332.1",
118
+ "version": "2.332.3",
119
119
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
120
120
  "main": "dist/index.js",
121
121
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -38,6 +38,7 @@ import {
38
38
  rmSync,
39
39
  writeFileSync,
40
40
  } from "node:fs";
41
+ import { homedir } from "node:os";
41
42
  import { delimiter, dirname, join, resolve } from "node:path";
42
43
  import { fileURLToPath, pathToFileURL } from "node:url";
43
44
 
@@ -583,6 +584,101 @@ export const SETUP_FIELD =
583
584
  'PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; ' +
584
585
  'exit "$rc"';
585
586
 
587
+ /**
588
+ * Register the session-start hook at USER scope, so it fires wherever the
589
+ * session opens.
590
+ *
591
+ * The committed `.claude/settings.json` hook is *project*-scoped: it loads only
592
+ * when Claude Code's project directory is that repository. A cloud session does
593
+ * not reliably start there. With several repositories it starts in the shared
594
+ * parent, which is not a git repository at all; even with one, the checkout sits
595
+ * at `$HOME/<repo>` while the session may open at `$HOME`. In those sessions no
596
+ * project settings load, the hook never registers, and nothing materializes —
597
+ * the failure that `materializeAt: "both"` was introduced to paper over, and
598
+ * which it could not fix, because the setup phase cannot see the bootstrap
599
+ * credential in the first place.
600
+ *
601
+ * A user-scoped hook has neither problem: `~/.claude/settings.json` is read for
602
+ * every session regardless of project directory, and it runs in-session where
603
+ * the vendor's configured variables ARE present.
604
+ *
605
+ * Only ever on a remote surface. This writes machine state in an ephemeral
606
+ * container, which is fine there and would be an intrusion on a developer's
607
+ * laptop, where the project hook already works and the user owns that file.
608
+ *
609
+ * Merged, never clobbered, and idempotent on the exact command — an existing
610
+ * user settings file may carry hooks this knows nothing about.
611
+ * @param {string} repoRoot Absolute path to the checkout.
612
+ * @param {object} options Home directory, dry-run flag, and file seams.
613
+ * @returns {{action: string, path: string, reason?: string}} What was done.
614
+ */
615
+ export function installUserSessionHook(repoRoot, options = {}) {
616
+ const {
617
+ home = process.env.HOME || homedir(),
618
+ dryRun = false,
619
+ exists = existsSync,
620
+ read = readFileSync,
621
+ write = writeFileSync,
622
+ mkdir = mkdirSync,
623
+ } = options;
624
+
625
+ const script = join(repoRoot, "scripts/lisa-remote-env/session-start.sh");
626
+ if (!exists(script)) {
627
+ return { action: "skipped", path: script, reason: "no session-start.sh" };
628
+ }
629
+
630
+ const dir = join(home, ".claude");
631
+ const settingsPath = join(dir, "settings.json");
632
+ const command = `bash ${script}`;
633
+
634
+ let settings = {};
635
+ if (exists(settingsPath)) {
636
+ try {
637
+ settings = JSON.parse(String(read(settingsPath, "utf8"))) || {};
638
+ } catch {
639
+ // Refuse rather than overwrite. A settings file that does not parse is
640
+ // still someone's configuration, and replacing it would be the careless
641
+ // destruction this whole function is written to avoid.
642
+ return {
643
+ action: "failed",
644
+ path: settingsPath,
645
+ reason: "existing settings.json is not valid JSON; left untouched",
646
+ };
647
+ }
648
+ }
649
+
650
+ const hooks = settings.hooks ?? {};
651
+ const sessionStart = Array.isArray(hooks.SessionStart)
652
+ ? hooks.SessionStart
653
+ : [];
654
+ const already = sessionStart.some(entry =>
655
+ (entry?.hooks ?? []).some(hook => hook?.command === command)
656
+ );
657
+ if (already) {
658
+ return { action: "present", path: settingsPath };
659
+ }
660
+
661
+ const updated = {
662
+ ...settings,
663
+ hooks: {
664
+ ...hooks,
665
+ SessionStart: [
666
+ ...sessionStart,
667
+ {
668
+ matcher: "startup|resume",
669
+ hooks: [{ type: "command", command }],
670
+ },
671
+ ],
672
+ },
673
+ };
674
+
675
+ if (dryRun) return { action: "would-register", path: settingsPath };
676
+
677
+ mkdir(dir, { recursive: true });
678
+ write(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
679
+ return { action: "registered", path: settingsPath };
680
+ }
681
+
586
682
  /**
587
683
  * The settings block that wires the session-start hook into a repository.
588
684
  *
@@ -914,9 +1010,58 @@ async function main() {
914
1010
  "lisa-secrets-access",
915
1011
  "materialize-secrets.mjs"
916
1012
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
1013
+ // A failure here is fatal only when nothing else will try again.
1014
+ //
1015
+ // On a surface that materializes at BOTH setup and session start, this run
1016
+ // is the floor and the hook is the authority. The two run in different
1017
+ // environments: a cloud vendor can expose its configured variables to the
1018
+ // agent session while the setup phase — which runs earlier, during image
1019
+ // construction — never sees them. That is not an error state, it is the
1020
+ // normal shape of `materializeAt: "both"`, and the hook materializes
1021
+ // correctly moments later.
1022
+ //
1023
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
1024
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
1025
+ // never starts at all. Trading "secrets arrive one phase later" for "the
1026
+ // container will not boot" is a bad trade, and it regressed a case that
1027
+ // used to work — before this surface materialized at setup, setup simply
1028
+ // never attempted it.
1029
+ //
1030
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
1031
+ // (the hook's own authoritative run) and a setup-only surface such as
1032
+ // codex-cloud both have no second chance, so a silent pass there would
1033
+ // hand back a session with no credentials and call it ready.
1034
+ const retried = !requested && materializesAtSessionStart(materializeAt);
1035
+
1036
+ // If the hook is what will retry, make sure the hook can actually fire.
1037
+ // Registering it at user scope is what makes the retry real rather than
1038
+ // assumed: the committed project hook does not load when the session opens
1039
+ // above the checkout, which is the norm in a cloud container.
1040
+ // `materializeAt` being set is what "remote surface" means here. The
1041
+ // toolchain phase derives a local `remote` from it, but that binding is
1042
+ // scoped to its own block and is not in scope in this one.
1043
+ if (retried && Boolean(materializeAt)) {
1044
+ const hook = installUserSessionHook(process.cwd(), { dryRun });
1045
+ console.log(` session-start hook: ${hook.action} (${hook.path})`);
1046
+ if (hook.reason) console.log(` ${hook.reason}`);
1047
+ }
1048
+ try {
1049
+ execFileSync(
1050
+ "node",
1051
+ dryRun ? [materialize, "--dry-run"] : [materialize],
1052
+ { stdio: "inherit" }
1053
+ );
1054
+ } catch (err) {
1055
+ if (!retried) throw err;
1056
+ console.log(
1057
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
1058
+ `here.\n This surface also materializes at session start, which runs ` +
1059
+ `with the\n session's own environment — the usual reason setup cannot ` +
1060
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
1061
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
1062
+ `to debug.`
1063
+ );
1064
+ }
920
1065
  } else if (!requested) {
921
1066
  console.log(
922
1067
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -38,6 +38,7 @@ import {
38
38
  rmSync,
39
39
  writeFileSync,
40
40
  } from "node:fs";
41
+ import { homedir } from "node:os";
41
42
  import { delimiter, dirname, join, resolve } from "node:path";
42
43
  import { fileURLToPath, pathToFileURL } from "node:url";
43
44
 
@@ -583,6 +584,101 @@ export const SETUP_FIELD =
583
584
  'PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; ' +
584
585
  'exit "$rc"';
585
586
 
587
+ /**
588
+ * Register the session-start hook at USER scope, so it fires wherever the
589
+ * session opens.
590
+ *
591
+ * The committed `.claude/settings.json` hook is *project*-scoped: it loads only
592
+ * when Claude Code's project directory is that repository. A cloud session does
593
+ * not reliably start there. With several repositories it starts in the shared
594
+ * parent, which is not a git repository at all; even with one, the checkout sits
595
+ * at `$HOME/<repo>` while the session may open at `$HOME`. In those sessions no
596
+ * project settings load, the hook never registers, and nothing materializes —
597
+ * the failure that `materializeAt: "both"` was introduced to paper over, and
598
+ * which it could not fix, because the setup phase cannot see the bootstrap
599
+ * credential in the first place.
600
+ *
601
+ * A user-scoped hook has neither problem: `~/.claude/settings.json` is read for
602
+ * every session regardless of project directory, and it runs in-session where
603
+ * the vendor's configured variables ARE present.
604
+ *
605
+ * Only ever on a remote surface. This writes machine state in an ephemeral
606
+ * container, which is fine there and would be an intrusion on a developer's
607
+ * laptop, where the project hook already works and the user owns that file.
608
+ *
609
+ * Merged, never clobbered, and idempotent on the exact command — an existing
610
+ * user settings file may carry hooks this knows nothing about.
611
+ * @param {string} repoRoot Absolute path to the checkout.
612
+ * @param {object} options Home directory, dry-run flag, and file seams.
613
+ * @returns {{action: string, path: string, reason?: string}} What was done.
614
+ */
615
+ export function installUserSessionHook(repoRoot, options = {}) {
616
+ const {
617
+ home = process.env.HOME || homedir(),
618
+ dryRun = false,
619
+ exists = existsSync,
620
+ read = readFileSync,
621
+ write = writeFileSync,
622
+ mkdir = mkdirSync,
623
+ } = options;
624
+
625
+ const script = join(repoRoot, "scripts/lisa-remote-env/session-start.sh");
626
+ if (!exists(script)) {
627
+ return { action: "skipped", path: script, reason: "no session-start.sh" };
628
+ }
629
+
630
+ const dir = join(home, ".claude");
631
+ const settingsPath = join(dir, "settings.json");
632
+ const command = `bash ${script}`;
633
+
634
+ let settings = {};
635
+ if (exists(settingsPath)) {
636
+ try {
637
+ settings = JSON.parse(String(read(settingsPath, "utf8"))) || {};
638
+ } catch {
639
+ // Refuse rather than overwrite. A settings file that does not parse is
640
+ // still someone's configuration, and replacing it would be the careless
641
+ // destruction this whole function is written to avoid.
642
+ return {
643
+ action: "failed",
644
+ path: settingsPath,
645
+ reason: "existing settings.json is not valid JSON; left untouched",
646
+ };
647
+ }
648
+ }
649
+
650
+ const hooks = settings.hooks ?? {};
651
+ const sessionStart = Array.isArray(hooks.SessionStart)
652
+ ? hooks.SessionStart
653
+ : [];
654
+ const already = sessionStart.some(entry =>
655
+ (entry?.hooks ?? []).some(hook => hook?.command === command)
656
+ );
657
+ if (already) {
658
+ return { action: "present", path: settingsPath };
659
+ }
660
+
661
+ const updated = {
662
+ ...settings,
663
+ hooks: {
664
+ ...hooks,
665
+ SessionStart: [
666
+ ...sessionStart,
667
+ {
668
+ matcher: "startup|resume",
669
+ hooks: [{ type: "command", command }],
670
+ },
671
+ ],
672
+ },
673
+ };
674
+
675
+ if (dryRun) return { action: "would-register", path: settingsPath };
676
+
677
+ mkdir(dir, { recursive: true });
678
+ write(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
679
+ return { action: "registered", path: settingsPath };
680
+ }
681
+
586
682
  /**
587
683
  * The settings block that wires the session-start hook into a repository.
588
684
  *
@@ -914,9 +1010,58 @@ async function main() {
914
1010
  "lisa-secrets-access",
915
1011
  "materialize-secrets.mjs"
916
1012
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
1013
+ // A failure here is fatal only when nothing else will try again.
1014
+ //
1015
+ // On a surface that materializes at BOTH setup and session start, this run
1016
+ // is the floor and the hook is the authority. The two run in different
1017
+ // environments: a cloud vendor can expose its configured variables to the
1018
+ // agent session while the setup phase — which runs earlier, during image
1019
+ // construction — never sees them. That is not an error state, it is the
1020
+ // normal shape of `materializeAt: "both"`, and the hook materializes
1021
+ // correctly moments later.
1022
+ //
1023
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
1024
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
1025
+ // never starts at all. Trading "secrets arrive one phase later" for "the
1026
+ // container will not boot" is a bad trade, and it regressed a case that
1027
+ // used to work — before this surface materialized at setup, setup simply
1028
+ // never attempted it.
1029
+ //
1030
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
1031
+ // (the hook's own authoritative run) and a setup-only surface such as
1032
+ // codex-cloud both have no second chance, so a silent pass there would
1033
+ // hand back a session with no credentials and call it ready.
1034
+ const retried = !requested && materializesAtSessionStart(materializeAt);
1035
+
1036
+ // If the hook is what will retry, make sure the hook can actually fire.
1037
+ // Registering it at user scope is what makes the retry real rather than
1038
+ // assumed: the committed project hook does not load when the session opens
1039
+ // above the checkout, which is the norm in a cloud container.
1040
+ // `materializeAt` being set is what "remote surface" means here. The
1041
+ // toolchain phase derives a local `remote` from it, but that binding is
1042
+ // scoped to its own block and is not in scope in this one.
1043
+ if (retried && Boolean(materializeAt)) {
1044
+ const hook = installUserSessionHook(process.cwd(), { dryRun });
1045
+ console.log(` session-start hook: ${hook.action} (${hook.path})`);
1046
+ if (hook.reason) console.log(` ${hook.reason}`);
1047
+ }
1048
+ try {
1049
+ execFileSync(
1050
+ "node",
1051
+ dryRun ? [materialize, "--dry-run"] : [materialize],
1052
+ { stdio: "inherit" }
1053
+ );
1054
+ } catch (err) {
1055
+ if (!retried) throw err;
1056
+ console.log(
1057
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
1058
+ `here.\n This surface also materializes at session start, which runs ` +
1059
+ `with the\n session's own environment — the usual reason setup cannot ` +
1060
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
1061
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
1062
+ `to debug.`
1063
+ );
1064
+ }
920
1065
  } else if (!requested) {
921
1066
  console.log(
922
1067
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -38,6 +38,7 @@ import {
38
38
  rmSync,
39
39
  writeFileSync,
40
40
  } from "node:fs";
41
+ import { homedir } from "node:os";
41
42
  import { delimiter, dirname, join, resolve } from "node:path";
42
43
  import { fileURLToPath, pathToFileURL } from "node:url";
43
44
 
@@ -583,6 +584,101 @@ export const SETUP_FIELD =
583
584
  'PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; ' +
584
585
  'exit "$rc"';
585
586
 
587
+ /**
588
+ * Register the session-start hook at USER scope, so it fires wherever the
589
+ * session opens.
590
+ *
591
+ * The committed `.claude/settings.json` hook is *project*-scoped: it loads only
592
+ * when Claude Code's project directory is that repository. A cloud session does
593
+ * not reliably start there. With several repositories it starts in the shared
594
+ * parent, which is not a git repository at all; even with one, the checkout sits
595
+ * at `$HOME/<repo>` while the session may open at `$HOME`. In those sessions no
596
+ * project settings load, the hook never registers, and nothing materializes —
597
+ * the failure that `materializeAt: "both"` was introduced to paper over, and
598
+ * which it could not fix, because the setup phase cannot see the bootstrap
599
+ * credential in the first place.
600
+ *
601
+ * A user-scoped hook has neither problem: `~/.claude/settings.json` is read for
602
+ * every session regardless of project directory, and it runs in-session where
603
+ * the vendor's configured variables ARE present.
604
+ *
605
+ * Only ever on a remote surface. This writes machine state in an ephemeral
606
+ * container, which is fine there and would be an intrusion on a developer's
607
+ * laptop, where the project hook already works and the user owns that file.
608
+ *
609
+ * Merged, never clobbered, and idempotent on the exact command — an existing
610
+ * user settings file may carry hooks this knows nothing about.
611
+ * @param {string} repoRoot Absolute path to the checkout.
612
+ * @param {object} options Home directory, dry-run flag, and file seams.
613
+ * @returns {{action: string, path: string, reason?: string}} What was done.
614
+ */
615
+ export function installUserSessionHook(repoRoot, options = {}) {
616
+ const {
617
+ home = process.env.HOME || homedir(),
618
+ dryRun = false,
619
+ exists = existsSync,
620
+ read = readFileSync,
621
+ write = writeFileSync,
622
+ mkdir = mkdirSync,
623
+ } = options;
624
+
625
+ const script = join(repoRoot, "scripts/lisa-remote-env/session-start.sh");
626
+ if (!exists(script)) {
627
+ return { action: "skipped", path: script, reason: "no session-start.sh" };
628
+ }
629
+
630
+ const dir = join(home, ".claude");
631
+ const settingsPath = join(dir, "settings.json");
632
+ const command = `bash ${script}`;
633
+
634
+ let settings = {};
635
+ if (exists(settingsPath)) {
636
+ try {
637
+ settings = JSON.parse(String(read(settingsPath, "utf8"))) || {};
638
+ } catch {
639
+ // Refuse rather than overwrite. A settings file that does not parse is
640
+ // still someone's configuration, and replacing it would be the careless
641
+ // destruction this whole function is written to avoid.
642
+ return {
643
+ action: "failed",
644
+ path: settingsPath,
645
+ reason: "existing settings.json is not valid JSON; left untouched",
646
+ };
647
+ }
648
+ }
649
+
650
+ const hooks = settings.hooks ?? {};
651
+ const sessionStart = Array.isArray(hooks.SessionStart)
652
+ ? hooks.SessionStart
653
+ : [];
654
+ const already = sessionStart.some(entry =>
655
+ (entry?.hooks ?? []).some(hook => hook?.command === command)
656
+ );
657
+ if (already) {
658
+ return { action: "present", path: settingsPath };
659
+ }
660
+
661
+ const updated = {
662
+ ...settings,
663
+ hooks: {
664
+ ...hooks,
665
+ SessionStart: [
666
+ ...sessionStart,
667
+ {
668
+ matcher: "startup|resume",
669
+ hooks: [{ type: "command", command }],
670
+ },
671
+ ],
672
+ },
673
+ };
674
+
675
+ if (dryRun) return { action: "would-register", path: settingsPath };
676
+
677
+ mkdir(dir, { recursive: true });
678
+ write(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
679
+ return { action: "registered", path: settingsPath };
680
+ }
681
+
586
682
  /**
587
683
  * The settings block that wires the session-start hook into a repository.
588
684
  *
@@ -914,9 +1010,58 @@ async function main() {
914
1010
  "lisa-secrets-access",
915
1011
  "materialize-secrets.mjs"
916
1012
  );
917
- execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
918
- stdio: "inherit",
919
- });
1013
+ // A failure here is fatal only when nothing else will try again.
1014
+ //
1015
+ // On a surface that materializes at BOTH setup and session start, this run
1016
+ // is the floor and the hook is the authority. The two run in different
1017
+ // environments: a cloud vendor can expose its configured variables to the
1018
+ // agent session while the setup phase — which runs earlier, during image
1019
+ // construction — never sees them. That is not an error state, it is the
1020
+ // normal shape of `materializeAt: "both"`, and the hook materializes
1021
+ // correctly moments later.
1022
+ //
1023
+ // Treating it as fatal cost an entire environment: setup.sh `exec`s this
1024
+ // script, so a non-zero exit fails the vendor's setup step and Claude Code
1025
+ // never starts at all. Trading "secrets arrive one phase later" for "the
1026
+ // container will not boot" is a bad trade, and it regressed a case that
1027
+ // used to work — before this surface materialized at setup, setup simply
1028
+ // never attempted it.
1029
+ //
1030
+ // Every other case keeps the hard failure: an explicit `--phase=secrets`
1031
+ // (the hook's own authoritative run) and a setup-only surface such as
1032
+ // codex-cloud both have no second chance, so a silent pass there would
1033
+ // hand back a session with no credentials and call it ready.
1034
+ const retried = !requested && materializesAtSessionStart(materializeAt);
1035
+
1036
+ // If the hook is what will retry, make sure the hook can actually fire.
1037
+ // Registering it at user scope is what makes the retry real rather than
1038
+ // assumed: the committed project hook does not load when the session opens
1039
+ // above the checkout, which is the norm in a cloud container.
1040
+ // `materializeAt` being set is what "remote surface" means here. The
1041
+ // toolchain phase derives a local `remote` from it, but that binding is
1042
+ // scoped to its own block and is not in scope in this one.
1043
+ if (retried && Boolean(materializeAt)) {
1044
+ const hook = installUserSessionHook(process.cwd(), { dryRun });
1045
+ console.log(` session-start hook: ${hook.action} (${hook.path})`);
1046
+ if (hook.reason) console.log(` ${hook.reason}`);
1047
+ }
1048
+ try {
1049
+ execFileSync(
1050
+ "node",
1051
+ dryRun ? [materialize, "--dry-run"] : [materialize],
1052
+ { stdio: "inherit" }
1053
+ );
1054
+ } catch (err) {
1055
+ if (!retried) throw err;
1056
+ console.log(
1057
+ `\n Secrets were not materialized during setup, and that is not fatal ` +
1058
+ `here.\n This surface also materializes at session start, which runs ` +
1059
+ `with the\n session's own environment — the usual reason setup cannot ` +
1060
+ `see the\n bootstrap credential. The session-start hook will retry.\n` +
1061
+ ` If secrets are still missing INSIDE a session, that run is the one ` +
1062
+ `to debug.`
1063
+ );
1064
+ }
920
1065
  } else if (!requested) {
921
1066
  console.log(
922
1067
  `\nSecrets: materialized from a session-start hook on "${surface}", ` +
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.332.1",
3
+ "version": "2.332.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"