@codyswann/lisa 4.40.0 → 4.41.0

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 (85) hide show
  1. package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +57 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +17 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +14 -0
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +28 -0
  5. package/dist/codex/scripts/block-no-verify.sh +21 -0
  6. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  7. package/dist/core/lisa-owned-hash-ledger.js +20 -0
  8. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  9. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  10. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  11. package/dist/core/upstream-evidence-manifest.js +13 -11
  12. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  13. package/dist/opencode/plugin-templates/block-managed-file-edits.sh +14 -0
  14. package/dist/opencode/plugin-templates/block-no-verify.sh +28 -0
  15. package/package.json +4 -4
  16. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa/hooks/block-direct-issue-create.sh +17 -0
  19. package/plugins/lisa/hooks/block-managed-file-edits.sh +14 -0
  20. package/plugins/lisa/hooks/block-no-verify.agy.sh +21 -0
  21. package/plugins/lisa/hooks/block-no-verify.sh +28 -0
  22. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +17 -0
  23. package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +14 -0
  24. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +21 -0
  25. package/plugins/lisa-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +17 -0
  33. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +14 -0
  34. package/plugins/lisa-copilot/hooks/block-no-verify.sh +28 -0
  35. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +17 -0
  37. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +14 -0
  38. package/plugins/lisa-cursor/hooks/block-no-verify.sh +28 -0
  39. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-agy/plugin.json +1 -1
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  62. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails-agy/plugin.json +1 -1
  67. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  72. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/src/base/hooks/block-direct-issue-create.sh +17 -0
  80. package/plugins/src/base/hooks/block-managed-file-edits.sh +14 -0
  81. package/plugins/src/base/hooks/block-no-verify.agy.sh +21 -0
  82. package/plugins/src/base/hooks/block-no-verify.sh +28 -0
  83. package/rails/copy-contents/scripts/lisa-mutation.sh +8 -1
  84. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +101 -1
  85. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +20 -14
@@ -719,6 +719,20 @@ def shell_script_operand(args):
719
719
  # not a script file.
720
720
  if not token.startswith("--") and "c" in token[1:]:
721
721
  return None
722
+ # `-n` is noexec: the shell READS and parses the file, then exits
723
+ # without executing a line of it. `bash -n <file>` is the one shape
724
+ # that puts a path at a command position while provably running
725
+ # nothing — the exact seam between reading and running, and this
726
+ # walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
727
+ # syntax check of any script mentioning a managed path was refused
728
+ # as though it were about to write one.
729
+ #
730
+ # `bash <file>` still executes and is still refused. Noexec is the
731
+ # distinction, not the program.
732
+ if token == "--noexec" or (
733
+ not token.startswith("--") and "n" in token[1:]
734
+ ):
735
+ return None
722
736
  continue
723
737
  return token
724
738
  return None
@@ -24,6 +24,13 @@
24
24
  # `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
25
25
  # `xargs` puts at a command position.
26
26
  #
27
+ # NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
28
+ # the file, then exits without running a line — so a syntax check is the one
29
+ # shape that puts a path at a command position while executing nothing. It used
30
+ # to be refused, which made `bash -n` unusable on this file, on every husky
31
+ # hook, and on most of this repository (#3781). The distinction is noexec, not
32
+ # the program: `bash nv.sh` still executes and is still refused.
33
+ #
27
34
  # The line below is what lets `lisa apply` tell a downstream copy of this guard
28
35
  # that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
29
36
  # "differs from mine", and guessing "behind" is how a fork's stronger guard gets
@@ -1031,6 +1038,10 @@ def resolve_script(token):
1031
1038
 
1032
1039
  SHELL_COMMAND_STRING = object()
1033
1040
  SHELL_STDIN = object()
1041
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
1042
+ # executing a line of it. Distinct from the two above because nothing runs at
1043
+ # all — there is no payload to scan and no stdin script to wait for.
1044
+ SHELL_NOEXEC = object()
1034
1045
 
1035
1046
 
1036
1047
  def shell_execution_operand(args):
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
1061
1072
  index += 1
1062
1073
  continue
1063
1074
  if token.startswith("--"):
1075
+ if token == "--noexec":
1076
+ return SHELL_NOEXEC
1064
1077
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
1065
1078
  opaque_option_seen = True
1066
1079
  index += 1
1067
1080
  continue
1068
1081
  if "c" in token[1:]:
1069
1082
  return SHELL_COMMAND_STRING
1083
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1084
+ # a command position while provably executing nothing, so it is the
1085
+ # exact seam between reading and running — and reach got it wrong in
1086
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1087
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1088
+ # this repository's own guards and every husky hook.
1089
+ #
1090
+ # `bash <file>` still executes and is still refused. Noexec is the
1091
+ # distinction, not the program: this is not a read-only exemption.
1092
+ if "n" in token[1:]:
1093
+ return SHELL_NOEXEC
1070
1094
  index += 1
1071
1095
  return args[index] if index < len(args) else SHELL_STDIN
1072
1096
 
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
1111
1135
  continue
1112
1136
  if operand is SHELL_COMMAND_STRING:
1113
1137
  continue
1138
+ if operand is SHELL_NOEXEC:
1139
+ # Nothing runs, so there is nothing to follow and nothing
1140
+ # to fail closed about.
1141
+ continue
1114
1142
  else:
1115
1143
  continue
1116
1144
  if operand is None:
package/package.json CHANGED
@@ -171,7 +171,7 @@
171
171
  "zod-validation-error": "^4.0.0"
172
172
  },
173
173
  "name": "@codyswann/lisa",
174
- "version": "4.40.0",
174
+ "version": "4.41.0",
175
175
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
176
176
  "main": "dist/index.js",
177
177
  "exports": {
@@ -319,7 +319,7 @@
319
319
  "test": "tests"
320
320
  },
321
321
  "types": "./dist/index.d.ts",
322
- "lisaReleaseCommit": "7b2e6ab5ac33dee1c4e21b0d55012f3b16f6587d",
323
- "gitHead": "7b2e6ab5ac33dee1c4e21b0d55012f3b16f6587d",
324
- "lisaReleaseTag": "v4.40.0"
322
+ "lisaReleaseCommit": "620ec616954b878092b32a9a356cc20d6972a334",
323
+ "gitHead": "620ec616954b878092b32a9a356cc20d6972a334",
324
+ "lisaReleaseTag": "v4.41.0"
325
325
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
1708
1708
  # ordinary shell options and do not consume the script path.
1709
1709
  if not head.startswith("--") and "c" in head[1:]:
1710
1710
  return None
1711
+ # `-n` is noexec: the shell READS and parses the file and then
1712
+ # exits without running a line of it. `bash -n <file>` is the
1713
+ # one shape that puts a path at a command position while
1714
+ # provably executing nothing, so it is the exact seam between
1715
+ # reading and running — and this walk got it wrong in BOTH
1716
+ # directions (CodySwannGT/lisa#3781). A creation-shaped file
1717
+ # earned a false refusal; a file carrying a human-gate marker
1718
+ # earned a false ALLOW, adjudicating as "declared" a command
1719
+ # that filed nothing. The silent direction is the worse one.
1720
+ #
1721
+ # Not a read-only-command exemption: `bash <file>` still runs
1722
+ # the file and is still refused. The distinction is noexec, not
1723
+ # the program.
1724
+ if head == "--noexec" or (
1725
+ not head.startswith("--") and "n" in head[1:]
1726
+ ):
1727
+ return None
1711
1728
  elif head in NO_SCRIPT_OPTIONS.get(program, set()):
1712
1729
  return None
1713
1730
  takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
@@ -719,6 +719,20 @@ def shell_script_operand(args):
719
719
  # not a script file.
720
720
  if not token.startswith("--") and "c" in token[1:]:
721
721
  return None
722
+ # `-n` is noexec: the shell READS and parses the file, then exits
723
+ # without executing a line of it. `bash -n <file>` is the one shape
724
+ # that puts a path at a command position while provably running
725
+ # nothing — the exact seam between reading and running, and this
726
+ # walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
727
+ # syntax check of any script mentioning a managed path was refused
728
+ # as though it were about to write one.
729
+ #
730
+ # `bash <file>` still executes and is still refused. Noexec is the
731
+ # distinction, not the program.
732
+ if token == "--noexec" or (
733
+ not token.startswith("--") and "n" in token[1:]
734
+ ):
735
+ return None
722
736
  continue
723
737
  return token
724
738
  return None
@@ -961,6 +961,10 @@ def resolve_script(token):
961
961
 
962
962
  SHELL_COMMAND_STRING = object()
963
963
  SHELL_STDIN = object()
964
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
965
+ # executing a line of it. Distinct from the two above because nothing runs at
966
+ # all — there is no payload to scan and no stdin script to wait for.
967
+ SHELL_NOEXEC = object()
964
968
 
965
969
 
966
970
  def shell_execution_operand(args):
@@ -991,12 +995,25 @@ def shell_execution_operand(args):
991
995
  index += 1
992
996
  continue
993
997
  if token.startswith("--"):
998
+ if token == "--noexec":
999
+ return SHELL_NOEXEC
994
1000
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
995
1001
  opaque_option_seen = True
996
1002
  index += 1
997
1003
  continue
998
1004
  if "c" in token[1:]:
999
1005
  return SHELL_COMMAND_STRING
1006
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1007
+ # a command position while provably executing nothing, so it is the
1008
+ # exact seam between reading and running — and reach got it wrong in
1009
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1010
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1011
+ # this repository's own guards and every husky hook.
1012
+ #
1013
+ # `bash <file>` still executes and is still refused. Noexec is the
1014
+ # distinction, not the program: this is not a read-only exemption.
1015
+ if "n" in token[1:]:
1016
+ return SHELL_NOEXEC
1000
1017
  index += 1
1001
1018
  return args[index] if index < len(args) else SHELL_STDIN
1002
1019
 
@@ -1041,6 +1058,10 @@ def executed_scripts(tokens):
1041
1058
  continue
1042
1059
  if operand is SHELL_COMMAND_STRING:
1043
1060
  continue
1061
+ if operand is SHELL_NOEXEC:
1062
+ # Nothing runs, so there is nothing to follow and nothing
1063
+ # to fail closed about.
1064
+ continue
1044
1065
  else:
1045
1066
  continue
1046
1067
  if operand is None:
@@ -24,6 +24,13 @@
24
24
  # `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
25
25
  # `xargs` puts at a command position.
26
26
  #
27
+ # NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
28
+ # the file, then exits without running a line — so a syntax check is the one
29
+ # shape that puts a path at a command position while executing nothing. It used
30
+ # to be refused, which made `bash -n` unusable on this file, on every husky
31
+ # hook, and on most of this repository (#3781). The distinction is noexec, not
32
+ # the program: `bash nv.sh` still executes and is still refused.
33
+ #
27
34
  # The line below is what lets `lisa apply` tell a downstream copy of this guard
28
35
  # that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
29
36
  # "differs from mine", and guessing "behind" is how a fork's stronger guard gets
@@ -1031,6 +1038,10 @@ def resolve_script(token):
1031
1038
 
1032
1039
  SHELL_COMMAND_STRING = object()
1033
1040
  SHELL_STDIN = object()
1041
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
1042
+ # executing a line of it. Distinct from the two above because nothing runs at
1043
+ # all — there is no payload to scan and no stdin script to wait for.
1044
+ SHELL_NOEXEC = object()
1034
1045
 
1035
1046
 
1036
1047
  def shell_execution_operand(args):
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
1061
1072
  index += 1
1062
1073
  continue
1063
1074
  if token.startswith("--"):
1075
+ if token == "--noexec":
1076
+ return SHELL_NOEXEC
1064
1077
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
1065
1078
  opaque_option_seen = True
1066
1079
  index += 1
1067
1080
  continue
1068
1081
  if "c" in token[1:]:
1069
1082
  return SHELL_COMMAND_STRING
1083
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1084
+ # a command position while provably executing nothing, so it is the
1085
+ # exact seam between reading and running — and reach got it wrong in
1086
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1087
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1088
+ # this repository's own guards and every husky hook.
1089
+ #
1090
+ # `bash <file>` still executes and is still refused. Noexec is the
1091
+ # distinction, not the program: this is not a read-only exemption.
1092
+ if "n" in token[1:]:
1093
+ return SHELL_NOEXEC
1070
1094
  index += 1
1071
1095
  return args[index] if index < len(args) else SHELL_STDIN
1072
1096
 
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
1111
1135
  continue
1112
1136
  if operand is SHELL_COMMAND_STRING:
1113
1137
  continue
1138
+ if operand is SHELL_NOEXEC:
1139
+ # Nothing runs, so there is nothing to follow and nothing
1140
+ # to fail closed about.
1141
+ continue
1114
1142
  else:
1115
1143
  continue
1116
1144
  if operand is None:
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
1708
1708
  # ordinary shell options and do not consume the script path.
1709
1709
  if not head.startswith("--") and "c" in head[1:]:
1710
1710
  return None
1711
+ # `-n` is noexec: the shell READS and parses the file and then
1712
+ # exits without running a line of it. `bash -n <file>` is the
1713
+ # one shape that puts a path at a command position while
1714
+ # provably executing nothing, so it is the exact seam between
1715
+ # reading and running — and this walk got it wrong in BOTH
1716
+ # directions (CodySwannGT/lisa#3781). A creation-shaped file
1717
+ # earned a false refusal; a file carrying a human-gate marker
1718
+ # earned a false ALLOW, adjudicating as "declared" a command
1719
+ # that filed nothing. The silent direction is the worse one.
1720
+ #
1721
+ # Not a read-only-command exemption: `bash <file>` still runs
1722
+ # the file and is still refused. The distinction is noexec, not
1723
+ # the program.
1724
+ if head == "--noexec" or (
1725
+ not head.startswith("--") and "n" in head[1:]
1726
+ ):
1727
+ return None
1711
1728
  elif head in NO_SCRIPT_OPTIONS.get(program, set()):
1712
1729
  return None
1713
1730
  takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
@@ -719,6 +719,20 @@ def shell_script_operand(args):
719
719
  # not a script file.
720
720
  if not token.startswith("--") and "c" in token[1:]:
721
721
  return None
722
+ # `-n` is noexec: the shell READS and parses the file, then exits
723
+ # without executing a line of it. `bash -n <file>` is the one shape
724
+ # that puts a path at a command position while provably running
725
+ # nothing — the exact seam between reading and running, and this
726
+ # walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
727
+ # syntax check of any script mentioning a managed path was refused
728
+ # as though it were about to write one.
729
+ #
730
+ # `bash <file>` still executes and is still refused. Noexec is the
731
+ # distinction, not the program.
732
+ if token == "--noexec" or (
733
+ not token.startswith("--") and "n" in token[1:]
734
+ ):
735
+ return None
722
736
  continue
723
737
  return token
724
738
  return None
@@ -961,6 +961,10 @@ def resolve_script(token):
961
961
 
962
962
  SHELL_COMMAND_STRING = object()
963
963
  SHELL_STDIN = object()
964
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
965
+ # executing a line of it. Distinct from the two above because nothing runs at
966
+ # all — there is no payload to scan and no stdin script to wait for.
967
+ SHELL_NOEXEC = object()
964
968
 
965
969
 
966
970
  def shell_execution_operand(args):
@@ -991,12 +995,25 @@ def shell_execution_operand(args):
991
995
  index += 1
992
996
  continue
993
997
  if token.startswith("--"):
998
+ if token == "--noexec":
999
+ return SHELL_NOEXEC
994
1000
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
995
1001
  opaque_option_seen = True
996
1002
  index += 1
997
1003
  continue
998
1004
  if "c" in token[1:]:
999
1005
  return SHELL_COMMAND_STRING
1006
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1007
+ # a command position while provably executing nothing, so it is the
1008
+ # exact seam between reading and running — and reach got it wrong in
1009
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1010
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1011
+ # this repository's own guards and every husky hook.
1012
+ #
1013
+ # `bash <file>` still executes and is still refused. Noexec is the
1014
+ # distinction, not the program: this is not a read-only exemption.
1015
+ if "n" in token[1:]:
1016
+ return SHELL_NOEXEC
1000
1017
  index += 1
1001
1018
  return args[index] if index < len(args) else SHELL_STDIN
1002
1019
 
@@ -1041,6 +1058,10 @@ def executed_scripts(tokens):
1041
1058
  continue
1042
1059
  if operand is SHELL_COMMAND_STRING:
1043
1060
  continue
1061
+ if operand is SHELL_NOEXEC:
1062
+ # Nothing runs, so there is nothing to follow and nothing
1063
+ # to fail closed about.
1064
+ continue
1044
1065
  else:
1045
1066
  continue
1046
1067
  if operand is None:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.40.0",
3
+ "version": "4.41.0",
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-cdk",
3
- "version": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
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": "4.40.0",
3
+ "version": "4.41.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
1708
1708
  # ordinary shell options and do not consume the script path.
1709
1709
  if not head.startswith("--") and "c" in head[1:]:
1710
1710
  return None
1711
+ # `-n` is noexec: the shell READS and parses the file and then
1712
+ # exits without running a line of it. `bash -n <file>` is the
1713
+ # one shape that puts a path at a command position while
1714
+ # provably executing nothing, so it is the exact seam between
1715
+ # reading and running — and this walk got it wrong in BOTH
1716
+ # directions (CodySwannGT/lisa#3781). A creation-shaped file
1717
+ # earned a false refusal; a file carrying a human-gate marker
1718
+ # earned a false ALLOW, adjudicating as "declared" a command
1719
+ # that filed nothing. The silent direction is the worse one.
1720
+ #
1721
+ # Not a read-only-command exemption: `bash <file>` still runs
1722
+ # the file and is still refused. The distinction is noexec, not
1723
+ # the program.
1724
+ if head == "--noexec" or (
1725
+ not head.startswith("--") and "n" in head[1:]
1726
+ ):
1727
+ return None
1711
1728
  elif head in NO_SCRIPT_OPTIONS.get(program, set()):
1712
1729
  return None
1713
1730
  takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
@@ -719,6 +719,20 @@ def shell_script_operand(args):
719
719
  # not a script file.
720
720
  if not token.startswith("--") and "c" in token[1:]:
721
721
  return None
722
+ # `-n` is noexec: the shell READS and parses the file, then exits
723
+ # without executing a line of it. `bash -n <file>` is the one shape
724
+ # that puts a path at a command position while provably running
725
+ # nothing — the exact seam between reading and running, and this
726
+ # walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
727
+ # syntax check of any script mentioning a managed path was refused
728
+ # as though it were about to write one.
729
+ #
730
+ # `bash <file>` still executes and is still refused. Noexec is the
731
+ # distinction, not the program.
732
+ if token == "--noexec" or (
733
+ not token.startswith("--") and "n" in token[1:]
734
+ ):
735
+ return None
722
736
  continue
723
737
  return token
724
738
  return None
@@ -24,6 +24,13 @@
24
24
  # `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
25
25
  # `xargs` puts at a command position.
26
26
  #
27
+ # NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
28
+ # the file, then exits without running a line — so a syntax check is the one
29
+ # shape that puts a path at a command position while executing nothing. It used
30
+ # to be refused, which made `bash -n` unusable on this file, on every husky
31
+ # hook, and on most of this repository (#3781). The distinction is noexec, not
32
+ # the program: `bash nv.sh` still executes and is still refused.
33
+ #
27
34
  # The line below is what lets `lisa apply` tell a downstream copy of this guard
28
35
  # that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
29
36
  # "differs from mine", and guessing "behind" is how a fork's stronger guard gets
@@ -1031,6 +1038,10 @@ def resolve_script(token):
1031
1038
 
1032
1039
  SHELL_COMMAND_STRING = object()
1033
1040
  SHELL_STDIN = object()
1041
+ # `-n` / `--noexec`: the shell READS and parses its operand, then exits without
1042
+ # executing a line of it. Distinct from the two above because nothing runs at
1043
+ # all — there is no payload to scan and no stdin script to wait for.
1044
+ SHELL_NOEXEC = object()
1034
1045
 
1035
1046
 
1036
1047
  def shell_execution_operand(args):
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
1061
1072
  index += 1
1062
1073
  continue
1063
1074
  if token.startswith("--"):
1075
+ if token == "--noexec":
1076
+ return SHELL_NOEXEC
1064
1077
  if token not in SHELL_LONG_OPTIONS_NO_VALUE:
1065
1078
  opaque_option_seen = True
1066
1079
  index += 1
1067
1080
  continue
1068
1081
  if "c" in token[1:]:
1069
1082
  return SHELL_COMMAND_STRING
1083
+ # `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
1084
+ # a command position while provably executing nothing, so it is the
1085
+ # exact seam between reading and running — and reach got it wrong in
1086
+ # BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
1087
+ # of any file merely DISCUSSING the bypass was refused, which is most of
1088
+ # this repository's own guards and every husky hook.
1089
+ #
1090
+ # `bash <file>` still executes and is still refused. Noexec is the
1091
+ # distinction, not the program: this is not a read-only exemption.
1092
+ if "n" in token[1:]:
1093
+ return SHELL_NOEXEC
1070
1094
  index += 1
1071
1095
  return args[index] if index < len(args) else SHELL_STDIN
1072
1096
 
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
1111
1135
  continue
1112
1136
  if operand is SHELL_COMMAND_STRING:
1113
1137
  continue
1138
+ if operand is SHELL_NOEXEC:
1139
+ # Nothing runs, so there is nothing to follow and nothing
1140
+ # to fail closed about.
1141
+ continue
1114
1142
  else:
1115
1143
  continue
1116
1144
  if operand is None:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.40.0",
3
+ "version": "4.41.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
1708
1708
  # ordinary shell options and do not consume the script path.
1709
1709
  if not head.startswith("--") and "c" in head[1:]:
1710
1710
  return None
1711
+ # `-n` is noexec: the shell READS and parses the file and then
1712
+ # exits without running a line of it. `bash -n <file>` is the
1713
+ # one shape that puts a path at a command position while
1714
+ # provably executing nothing, so it is the exact seam between
1715
+ # reading and running — and this walk got it wrong in BOTH
1716
+ # directions (CodySwannGT/lisa#3781). A creation-shaped file
1717
+ # earned a false refusal; a file carrying a human-gate marker
1718
+ # earned a false ALLOW, adjudicating as "declared" a command
1719
+ # that filed nothing. The silent direction is the worse one.
1720
+ #
1721
+ # Not a read-only-command exemption: `bash <file>` still runs
1722
+ # the file and is still refused. The distinction is noexec, not
1723
+ # the program.
1724
+ if head == "--noexec" or (
1725
+ not head.startswith("--") and "n" in head[1:]
1726
+ ):
1727
+ return None
1711
1728
  elif head in NO_SCRIPT_OPTIONS.get(program, set()):
1712
1729
  return None
1713
1730
  takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
@@ -719,6 +719,20 @@ def shell_script_operand(args):
719
719
  # not a script file.
720
720
  if not token.startswith("--") and "c" in token[1:]:
721
721
  return None
722
+ # `-n` is noexec: the shell READS and parses the file, then exits
723
+ # without executing a line of it. `bash -n <file>` is the one shape
724
+ # that puts a path at a command position while provably running
725
+ # nothing — the exact seam between reading and running, and this
726
+ # walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
727
+ # syntax check of any script mentioning a managed path was refused
728
+ # as though it were about to write one.
729
+ #
730
+ # `bash <file>` still executes and is still refused. Noexec is the
731
+ # distinction, not the program.
732
+ if token == "--noexec" or (
733
+ not token.startswith("--") and "n" in token[1:]
734
+ ):
735
+ return None
722
736
  continue
723
737
  return token
724
738
  return None