@codyswann/lisa 4.4.8 → 4.4.10

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 (67) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +78 -1
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +1 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +8 -4
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/dist/opencode/plugin-templates/parity-safety-net.sh +78 -1
  9. package/package.json +1 -1
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/hooks/parity-safety-net.sh +78 -1
  13. package/plugins/lisa-agy/hooks/parity-safety-net.sh +78 -1
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +78 -1
  22. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +78 -1
  24. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/plugin.json +1 -1
  27. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  47. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-agy/plugin.json +1 -1
  52. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  57. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  62. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/src/base/hooks/parity-safety-net.sh +78 -1
  65. package/scripts/lib/plugin-cache-resolution.mjs +325 -0
  66. package/scripts/plugin-parity-drift.mjs +20 -253
  67. package/scripts/plugin-routing-validate.mjs +32 -68
@@ -558,7 +558,84 @@ if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(tabl
558
558
  block "destructive SQL (DROP/TRUNCATE) detected"
559
559
  fi
560
560
 
561
- # 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
561
+ # 14. The git control plane. A recursive forced delete of `.git` destroys every
562
+ # commit, branch and stash that is not already pushed — the one directory whose
563
+ # loss is not recoverable from the working tree.
564
+ #
565
+ # This is not a new principle, it is a hole in one the guard already holds. Rule
566
+ # 4 above already refuses `rm -rf "$SOMEDIR"` and paths outside the project;
567
+ # `rm -rf .git` was permitted only because nothing named it. Measured before
568
+ # this rule existed: `rm -rf .git`, `rm -rf ./.git` and `rm -rf .git/objects`
569
+ # were all ALLOWED while the variable-expanded case was correctly blocked.
570
+ #
571
+ # `.git` must be a whole path component. `.gitignore`, `.gitattributes`,
572
+ # `.github/` and a `.git-old` backup are ordinary files a project deletes on
573
+ # purpose, and blocking those would be the collision that gets a guard switched
574
+ # off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
575
+ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
576
+ if matches_cs "$RM_RF_CLUSTER" || matches_cs "$RM_RF_SPLIT"; then
577
+ if matches_cs "$GIT_CONTROL_PLANE"; then
578
+ block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
579
+ fi
580
+ fi
581
+
582
+ # 15. Credential stores. Reading one into an agent transcript is disclosure even
583
+ # when nothing is copied anywhere: the value lands in a log, a context window,
584
+ # and whatever retains either.
585
+ #
586
+ # Scoped deliberately rather than by the obvious pattern. Each clause below is a
587
+ # separate judgement about a family where an agent has no legitimate read, and
588
+ # each carries the exclusions that keep ordinary work moving — public keys,
589
+ # known_hosts, ssh config, and `.env.example` are all still readable, because a
590
+ # guard that blocks routine setup is a guard somebody disables.
591
+
592
+ # The verbs that disclose: read it, duplicate it, or send it. Writing is NOT
593
+ # here — `echo FOO=1 > .env` during setup creates a secret rather than leaking
594
+ # one, and refusing it would block the very workflow that produces the file.
595
+ readonly SECRET_READ_VERB='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(cat|bat|less|more|head|tail|strings|xxd|od|base64|cp|mv|rsync|scp|sftp|tar|zip|curl|wget|http|nc)([[:space:]]|$)'
596
+
597
+ # 15a. SSH PRIVATE keys. `id_rsa.pub` and friends are public by construction and
598
+ # stay readable; so do `known_hosts`, `config` and `authorized_keys`, which
599
+ # agents legitimately inspect when diagnosing a remote.
600
+ readonly SSH_PRIVATE_KEY='\.ssh/(id_[[:alnum:]_-]+|[[:alnum:]_.-]*_(rsa|dsa|ecdsa|ed25519))([[:space:]'"$qc"']|$)'
601
+ # 15b. Cloud provider credential files.
602
+ readonly CLOUD_CREDENTIAL='(\.aws/(credentials|config)|\.config/gcloud/[[:alnum:]_./-]*credentials[[:alnum:]_.-]*|\.azure/(accessTokens|msal_token_cache)[[:alnum:]_.-]*|\.kube/config)([[:space:]'"$qc"']|$)'
603
+ # 15c. Coding-agent credential stores — the tokens that impersonate the operator
604
+ # to every service their agent can reach.
605
+ readonly AGENT_CREDENTIAL='(\.(claude|codex|cursor|copilot|gemini|antigravity)/[[:alnum:]_./-]*(credentials|auth|token)[[:alnum:]_.-]*|\.netrc|\.npmrc|\.pypirc|\.docker/config\.json)([[:space:]'"$qc"']|$)'
606
+ # 15d. Dotenv files. `.env.example`, `.sample`, `.template`, `.dist` and
607
+ # `.schema` are checked-in documentation of which keys exist, never the values,
608
+ # so they are excluded — that collision is the one this rule most had to avoid.
609
+ readonly DOTENV_SECRET='(^|[[:space:]='"$qc"'./])\.env(\.[[:alnum:]_-]+)?([[:space:]'"$qc"']|$)'
610
+ readonly DOTENV_PUBLIC='\.env\.(example|sample|template|dist|schema|defaults)([[:space:]'"$qc"']|$)'
611
+
612
+ if matches "$SECRET_READ_VERB"; then
613
+ if matches "$SSH_PRIVATE_KEY"; then
614
+ block "reading, copying or transmitting an SSH PRIVATE key. Public keys (.pub), known_hosts and ssh config are unaffected; if you need the fingerprint use \`ssh-keygen -lf <key>.pub\`."
615
+ fi
616
+ if matches "$CLOUD_CREDENTIAL"; then
617
+ block "reading, copying or transmitting a cloud provider credential file. Use the provider CLI's own identity command (\`aws sts get-caller-identity\`, \`gcloud auth list\`) instead of reading the file."
618
+ fi
619
+ if matches "$AGENT_CREDENTIAL"; then
620
+ block "reading, copying or transmitting a coding-agent or package-registry credential store. These tokens impersonate the operator to every service the agent can reach."
621
+ fi
622
+ # `.env` only when the path is NOT one of the public example forms. Ordered as
623
+ # an exclusion rather than a narrower pattern so a new example suffix is one
624
+ # word to add, and so the reason a file is exempt stays readable.
625
+ if matches "$DOTENV_SECRET" && ! matches "$DOTENV_PUBLIC"; then
626
+ block "reading, copying or transmitting a dotenv file holding real values. \`.env.example\`, \`.env.sample\`, \`.env.template\` and \`.env.dist\` are readable, and WRITING a .env is unaffected — only reading one back is refused."
627
+ fi
628
+ # Fail-closed, narrowly. A read whose DIRECTORY is a known credential store
629
+ # but whose leaf is variable-expanded cannot be classified, and #2980's lesson
630
+ # is that a guard which cannot classify must refuse rather than permit. Scoped
631
+ # to credential directories on purpose: refusing every \`cat "\$FILE"\` would be
632
+ # the over-reach that gets the whole hook switched off.
633
+ if matches_cs '(\.ssh|\.aws|\.config/gcloud|\.azure|\.kube)/[^[:space:]'"$qc"']*\$'; then
634
+ block "reading a path inside a credential directory whose filename is variable-expanded, so the guard cannot tell which file it is. Name the file explicitly if it is genuinely not a secret."
635
+ fi
636
+ fi
637
+
638
+ # 16. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
562
639
  rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
563
640
  if [ -f "$rules_file" ]; then
564
641
  while IFS= read -r rule || [ -n "$rule" ]; do
package/package.json CHANGED
@@ -145,7 +145,7 @@
145
145
  "zod-validation-error": "^4.0.0"
146
146
  },
147
147
  "name": "@codyswann/lisa",
148
- "version": "4.4.8",
148
+ "version": "4.4.10",
149
149
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
150
150
  "main": "dist/index.js",
151
151
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -558,7 +558,84 @@ if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(tabl
558
558
  block "destructive SQL (DROP/TRUNCATE) detected"
559
559
  fi
560
560
 
561
- # 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
561
+ # 14. The git control plane. A recursive forced delete of `.git` destroys every
562
+ # commit, branch and stash that is not already pushed — the one directory whose
563
+ # loss is not recoverable from the working tree.
564
+ #
565
+ # This is not a new principle, it is a hole in one the guard already holds. Rule
566
+ # 4 above already refuses `rm -rf "$SOMEDIR"` and paths outside the project;
567
+ # `rm -rf .git` was permitted only because nothing named it. Measured before
568
+ # this rule existed: `rm -rf .git`, `rm -rf ./.git` and `rm -rf .git/objects`
569
+ # were all ALLOWED while the variable-expanded case was correctly blocked.
570
+ #
571
+ # `.git` must be a whole path component. `.gitignore`, `.gitattributes`,
572
+ # `.github/` and a `.git-old` backup are ordinary files a project deletes on
573
+ # purpose, and blocking those would be the collision that gets a guard switched
574
+ # off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
575
+ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
576
+ if matches_cs "$RM_RF_CLUSTER" || matches_cs "$RM_RF_SPLIT"; then
577
+ if matches_cs "$GIT_CONTROL_PLANE"; then
578
+ block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
579
+ fi
580
+ fi
581
+
582
+ # 15. Credential stores. Reading one into an agent transcript is disclosure even
583
+ # when nothing is copied anywhere: the value lands in a log, a context window,
584
+ # and whatever retains either.
585
+ #
586
+ # Scoped deliberately rather than by the obvious pattern. Each clause below is a
587
+ # separate judgement about a family where an agent has no legitimate read, and
588
+ # each carries the exclusions that keep ordinary work moving — public keys,
589
+ # known_hosts, ssh config, and `.env.example` are all still readable, because a
590
+ # guard that blocks routine setup is a guard somebody disables.
591
+
592
+ # The verbs that disclose: read it, duplicate it, or send it. Writing is NOT
593
+ # here — `echo FOO=1 > .env` during setup creates a secret rather than leaking
594
+ # one, and refusing it would block the very workflow that produces the file.
595
+ readonly SECRET_READ_VERB='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(cat|bat|less|more|head|tail|strings|xxd|od|base64|cp|mv|rsync|scp|sftp|tar|zip|curl|wget|http|nc)([[:space:]]|$)'
596
+
597
+ # 15a. SSH PRIVATE keys. `id_rsa.pub` and friends are public by construction and
598
+ # stay readable; so do `known_hosts`, `config` and `authorized_keys`, which
599
+ # agents legitimately inspect when diagnosing a remote.
600
+ readonly SSH_PRIVATE_KEY='\.ssh/(id_[[:alnum:]_-]+|[[:alnum:]_.-]*_(rsa|dsa|ecdsa|ed25519))([[:space:]'"$qc"']|$)'
601
+ # 15b. Cloud provider credential files.
602
+ readonly CLOUD_CREDENTIAL='(\.aws/(credentials|config)|\.config/gcloud/[[:alnum:]_./-]*credentials[[:alnum:]_.-]*|\.azure/(accessTokens|msal_token_cache)[[:alnum:]_.-]*|\.kube/config)([[:space:]'"$qc"']|$)'
603
+ # 15c. Coding-agent credential stores — the tokens that impersonate the operator
604
+ # to every service their agent can reach.
605
+ readonly AGENT_CREDENTIAL='(\.(claude|codex|cursor|copilot|gemini|antigravity)/[[:alnum:]_./-]*(credentials|auth|token)[[:alnum:]_.-]*|\.netrc|\.npmrc|\.pypirc|\.docker/config\.json)([[:space:]'"$qc"']|$)'
606
+ # 15d. Dotenv files. `.env.example`, `.sample`, `.template`, `.dist` and
607
+ # `.schema` are checked-in documentation of which keys exist, never the values,
608
+ # so they are excluded — that collision is the one this rule most had to avoid.
609
+ readonly DOTENV_SECRET='(^|[[:space:]='"$qc"'./])\.env(\.[[:alnum:]_-]+)?([[:space:]'"$qc"']|$)'
610
+ readonly DOTENV_PUBLIC='\.env\.(example|sample|template|dist|schema|defaults)([[:space:]'"$qc"']|$)'
611
+
612
+ if matches "$SECRET_READ_VERB"; then
613
+ if matches "$SSH_PRIVATE_KEY"; then
614
+ block "reading, copying or transmitting an SSH PRIVATE key. Public keys (.pub), known_hosts and ssh config are unaffected; if you need the fingerprint use \`ssh-keygen -lf <key>.pub\`."
615
+ fi
616
+ if matches "$CLOUD_CREDENTIAL"; then
617
+ block "reading, copying or transmitting a cloud provider credential file. Use the provider CLI's own identity command (\`aws sts get-caller-identity\`, \`gcloud auth list\`) instead of reading the file."
618
+ fi
619
+ if matches "$AGENT_CREDENTIAL"; then
620
+ block "reading, copying or transmitting a coding-agent or package-registry credential store. These tokens impersonate the operator to every service the agent can reach."
621
+ fi
622
+ # `.env` only when the path is NOT one of the public example forms. Ordered as
623
+ # an exclusion rather than a narrower pattern so a new example suffix is one
624
+ # word to add, and so the reason a file is exempt stays readable.
625
+ if matches "$DOTENV_SECRET" && ! matches "$DOTENV_PUBLIC"; then
626
+ block "reading, copying or transmitting a dotenv file holding real values. \`.env.example\`, \`.env.sample\`, \`.env.template\` and \`.env.dist\` are readable, and WRITING a .env is unaffected — only reading one back is refused."
627
+ fi
628
+ # Fail-closed, narrowly. A read whose DIRECTORY is a known credential store
629
+ # but whose leaf is variable-expanded cannot be classified, and #2980's lesson
630
+ # is that a guard which cannot classify must refuse rather than permit. Scoped
631
+ # to credential directories on purpose: refusing every \`cat "\$FILE"\` would be
632
+ # the over-reach that gets the whole hook switched off.
633
+ if matches_cs '(\.ssh|\.aws|\.config/gcloud|\.azure|\.kube)/[^[:space:]'"$qc"']*\$'; then
634
+ block "reading a path inside a credential directory whose filename is variable-expanded, so the guard cannot tell which file it is. Name the file explicitly if it is genuinely not a secret."
635
+ fi
636
+ fi
637
+
638
+ # 16. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
562
639
  rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
563
640
  if [ -f "$rules_file" ]; then
564
641
  while IFS= read -r rule || [ -n "$rule" ]; do
@@ -558,7 +558,84 @@ if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(tabl
558
558
  block "destructive SQL (DROP/TRUNCATE) detected"
559
559
  fi
560
560
 
561
- # 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
561
+ # 14. The git control plane. A recursive forced delete of `.git` destroys every
562
+ # commit, branch and stash that is not already pushed — the one directory whose
563
+ # loss is not recoverable from the working tree.
564
+ #
565
+ # This is not a new principle, it is a hole in one the guard already holds. Rule
566
+ # 4 above already refuses `rm -rf "$SOMEDIR"` and paths outside the project;
567
+ # `rm -rf .git` was permitted only because nothing named it. Measured before
568
+ # this rule existed: `rm -rf .git`, `rm -rf ./.git` and `rm -rf .git/objects`
569
+ # were all ALLOWED while the variable-expanded case was correctly blocked.
570
+ #
571
+ # `.git` must be a whole path component. `.gitignore`, `.gitattributes`,
572
+ # `.github/` and a `.git-old` backup are ordinary files a project deletes on
573
+ # purpose, and blocking those would be the collision that gets a guard switched
574
+ # off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
575
+ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
576
+ if matches_cs "$RM_RF_CLUSTER" || matches_cs "$RM_RF_SPLIT"; then
577
+ if matches_cs "$GIT_CONTROL_PLANE"; then
578
+ block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
579
+ fi
580
+ fi
581
+
582
+ # 15. Credential stores. Reading one into an agent transcript is disclosure even
583
+ # when nothing is copied anywhere: the value lands in a log, a context window,
584
+ # and whatever retains either.
585
+ #
586
+ # Scoped deliberately rather than by the obvious pattern. Each clause below is a
587
+ # separate judgement about a family where an agent has no legitimate read, and
588
+ # each carries the exclusions that keep ordinary work moving — public keys,
589
+ # known_hosts, ssh config, and `.env.example` are all still readable, because a
590
+ # guard that blocks routine setup is a guard somebody disables.
591
+
592
+ # The verbs that disclose: read it, duplicate it, or send it. Writing is NOT
593
+ # here — `echo FOO=1 > .env` during setup creates a secret rather than leaking
594
+ # one, and refusing it would block the very workflow that produces the file.
595
+ readonly SECRET_READ_VERB='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(cat|bat|less|more|head|tail|strings|xxd|od|base64|cp|mv|rsync|scp|sftp|tar|zip|curl|wget|http|nc)([[:space:]]|$)'
596
+
597
+ # 15a. SSH PRIVATE keys. `id_rsa.pub` and friends are public by construction and
598
+ # stay readable; so do `known_hosts`, `config` and `authorized_keys`, which
599
+ # agents legitimately inspect when diagnosing a remote.
600
+ readonly SSH_PRIVATE_KEY='\.ssh/(id_[[:alnum:]_-]+|[[:alnum:]_.-]*_(rsa|dsa|ecdsa|ed25519))([[:space:]'"$qc"']|$)'
601
+ # 15b. Cloud provider credential files.
602
+ readonly CLOUD_CREDENTIAL='(\.aws/(credentials|config)|\.config/gcloud/[[:alnum:]_./-]*credentials[[:alnum:]_.-]*|\.azure/(accessTokens|msal_token_cache)[[:alnum:]_.-]*|\.kube/config)([[:space:]'"$qc"']|$)'
603
+ # 15c. Coding-agent credential stores — the tokens that impersonate the operator
604
+ # to every service their agent can reach.
605
+ readonly AGENT_CREDENTIAL='(\.(claude|codex|cursor|copilot|gemini|antigravity)/[[:alnum:]_./-]*(credentials|auth|token)[[:alnum:]_.-]*|\.netrc|\.npmrc|\.pypirc|\.docker/config\.json)([[:space:]'"$qc"']|$)'
606
+ # 15d. Dotenv files. `.env.example`, `.sample`, `.template`, `.dist` and
607
+ # `.schema` are checked-in documentation of which keys exist, never the values,
608
+ # so they are excluded — that collision is the one this rule most had to avoid.
609
+ readonly DOTENV_SECRET='(^|[[:space:]='"$qc"'./])\.env(\.[[:alnum:]_-]+)?([[:space:]'"$qc"']|$)'
610
+ readonly DOTENV_PUBLIC='\.env\.(example|sample|template|dist|schema|defaults)([[:space:]'"$qc"']|$)'
611
+
612
+ if matches "$SECRET_READ_VERB"; then
613
+ if matches "$SSH_PRIVATE_KEY"; then
614
+ block "reading, copying or transmitting an SSH PRIVATE key. Public keys (.pub), known_hosts and ssh config are unaffected; if you need the fingerprint use \`ssh-keygen -lf <key>.pub\`."
615
+ fi
616
+ if matches "$CLOUD_CREDENTIAL"; then
617
+ block "reading, copying or transmitting a cloud provider credential file. Use the provider CLI's own identity command (\`aws sts get-caller-identity\`, \`gcloud auth list\`) instead of reading the file."
618
+ fi
619
+ if matches "$AGENT_CREDENTIAL"; then
620
+ block "reading, copying or transmitting a coding-agent or package-registry credential store. These tokens impersonate the operator to every service the agent can reach."
621
+ fi
622
+ # `.env` only when the path is NOT one of the public example forms. Ordered as
623
+ # an exclusion rather than a narrower pattern so a new example suffix is one
624
+ # word to add, and so the reason a file is exempt stays readable.
625
+ if matches "$DOTENV_SECRET" && ! matches "$DOTENV_PUBLIC"; then
626
+ block "reading, copying or transmitting a dotenv file holding real values. \`.env.example\`, \`.env.sample\`, \`.env.template\` and \`.env.dist\` are readable, and WRITING a .env is unaffected — only reading one back is refused."
627
+ fi
628
+ # Fail-closed, narrowly. A read whose DIRECTORY is a known credential store
629
+ # but whose leaf is variable-expanded cannot be classified, and #2980's lesson
630
+ # is that a guard which cannot classify must refuse rather than permit. Scoped
631
+ # to credential directories on purpose: refusing every \`cat "\$FILE"\` would be
632
+ # the over-reach that gets the whole hook switched off.
633
+ if matches_cs '(\.ssh|\.aws|\.config/gcloud|\.azure|\.kube)/[^[:space:]'"$qc"']*\$'; then
634
+ block "reading a path inside a credential directory whose filename is variable-expanded, so the guard cannot tell which file it is. Name the file explicitly if it is genuinely not a secret."
635
+ fi
636
+ fi
637
+
638
+ # 16. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
562
639
  rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
563
640
  if [ -f "$rules_file" ]; then
564
641
  while IFS= read -r rule || [ -n "$rule" ]; do
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
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.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -558,7 +558,84 @@ if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(tabl
558
558
  block "destructive SQL (DROP/TRUNCATE) detected"
559
559
  fi
560
560
 
561
- # 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
561
+ # 14. The git control plane. A recursive forced delete of `.git` destroys every
562
+ # commit, branch and stash that is not already pushed — the one directory whose
563
+ # loss is not recoverable from the working tree.
564
+ #
565
+ # This is not a new principle, it is a hole in one the guard already holds. Rule
566
+ # 4 above already refuses `rm -rf "$SOMEDIR"` and paths outside the project;
567
+ # `rm -rf .git` was permitted only because nothing named it. Measured before
568
+ # this rule existed: `rm -rf .git`, `rm -rf ./.git` and `rm -rf .git/objects`
569
+ # were all ALLOWED while the variable-expanded case was correctly blocked.
570
+ #
571
+ # `.git` must be a whole path component. `.gitignore`, `.gitattributes`,
572
+ # `.github/` and a `.git-old` backup are ordinary files a project deletes on
573
+ # purpose, and blocking those would be the collision that gets a guard switched
574
+ # off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
575
+ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
576
+ if matches_cs "$RM_RF_CLUSTER" || matches_cs "$RM_RF_SPLIT"; then
577
+ if matches_cs "$GIT_CONTROL_PLANE"; then
578
+ block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
579
+ fi
580
+ fi
581
+
582
+ # 15. Credential stores. Reading one into an agent transcript is disclosure even
583
+ # when nothing is copied anywhere: the value lands in a log, a context window,
584
+ # and whatever retains either.
585
+ #
586
+ # Scoped deliberately rather than by the obvious pattern. Each clause below is a
587
+ # separate judgement about a family where an agent has no legitimate read, and
588
+ # each carries the exclusions that keep ordinary work moving — public keys,
589
+ # known_hosts, ssh config, and `.env.example` are all still readable, because a
590
+ # guard that blocks routine setup is a guard somebody disables.
591
+
592
+ # The verbs that disclose: read it, duplicate it, or send it. Writing is NOT
593
+ # here — `echo FOO=1 > .env` during setup creates a secret rather than leaking
594
+ # one, and refusing it would block the very workflow that produces the file.
595
+ readonly SECRET_READ_VERB='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(cat|bat|less|more|head|tail|strings|xxd|od|base64|cp|mv|rsync|scp|sftp|tar|zip|curl|wget|http|nc)([[:space:]]|$)'
596
+
597
+ # 15a. SSH PRIVATE keys. `id_rsa.pub` and friends are public by construction and
598
+ # stay readable; so do `known_hosts`, `config` and `authorized_keys`, which
599
+ # agents legitimately inspect when diagnosing a remote.
600
+ readonly SSH_PRIVATE_KEY='\.ssh/(id_[[:alnum:]_-]+|[[:alnum:]_.-]*_(rsa|dsa|ecdsa|ed25519))([[:space:]'"$qc"']|$)'
601
+ # 15b. Cloud provider credential files.
602
+ readonly CLOUD_CREDENTIAL='(\.aws/(credentials|config)|\.config/gcloud/[[:alnum:]_./-]*credentials[[:alnum:]_.-]*|\.azure/(accessTokens|msal_token_cache)[[:alnum:]_.-]*|\.kube/config)([[:space:]'"$qc"']|$)'
603
+ # 15c. Coding-agent credential stores — the tokens that impersonate the operator
604
+ # to every service their agent can reach.
605
+ readonly AGENT_CREDENTIAL='(\.(claude|codex|cursor|copilot|gemini|antigravity)/[[:alnum:]_./-]*(credentials|auth|token)[[:alnum:]_.-]*|\.netrc|\.npmrc|\.pypirc|\.docker/config\.json)([[:space:]'"$qc"']|$)'
606
+ # 15d. Dotenv files. `.env.example`, `.sample`, `.template`, `.dist` and
607
+ # `.schema` are checked-in documentation of which keys exist, never the values,
608
+ # so they are excluded — that collision is the one this rule most had to avoid.
609
+ readonly DOTENV_SECRET='(^|[[:space:]='"$qc"'./])\.env(\.[[:alnum:]_-]+)?([[:space:]'"$qc"']|$)'
610
+ readonly DOTENV_PUBLIC='\.env\.(example|sample|template|dist|schema|defaults)([[:space:]'"$qc"']|$)'
611
+
612
+ if matches "$SECRET_READ_VERB"; then
613
+ if matches "$SSH_PRIVATE_KEY"; then
614
+ block "reading, copying or transmitting an SSH PRIVATE key. Public keys (.pub), known_hosts and ssh config are unaffected; if you need the fingerprint use \`ssh-keygen -lf <key>.pub\`."
615
+ fi
616
+ if matches "$CLOUD_CREDENTIAL"; then
617
+ block "reading, copying or transmitting a cloud provider credential file. Use the provider CLI's own identity command (\`aws sts get-caller-identity\`, \`gcloud auth list\`) instead of reading the file."
618
+ fi
619
+ if matches "$AGENT_CREDENTIAL"; then
620
+ block "reading, copying or transmitting a coding-agent or package-registry credential store. These tokens impersonate the operator to every service the agent can reach."
621
+ fi
622
+ # `.env` only when the path is NOT one of the public example forms. Ordered as
623
+ # an exclusion rather than a narrower pattern so a new example suffix is one
624
+ # word to add, and so the reason a file is exempt stays readable.
625
+ if matches "$DOTENV_SECRET" && ! matches "$DOTENV_PUBLIC"; then
626
+ block "reading, copying or transmitting a dotenv file holding real values. \`.env.example\`, \`.env.sample\`, \`.env.template\` and \`.env.dist\` are readable, and WRITING a .env is unaffected — only reading one back is refused."
627
+ fi
628
+ # Fail-closed, narrowly. A read whose DIRECTORY is a known credential store
629
+ # but whose leaf is variable-expanded cannot be classified, and #2980's lesson
630
+ # is that a guard which cannot classify must refuse rather than permit. Scoped
631
+ # to credential directories on purpose: refusing every \`cat "\$FILE"\` would be
632
+ # the over-reach that gets the whole hook switched off.
633
+ if matches_cs '(\.ssh|\.aws|\.config/gcloud|\.azure|\.kube)/[^[:space:]'"$qc"']*\$'; then
634
+ block "reading a path inside a credential directory whose filename is variable-expanded, so the guard cannot tell which file it is. Name the file explicitly if it is genuinely not a secret."
635
+ fi
636
+ fi
637
+
638
+ # 16. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
562
639
  rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
563
640
  if [ -f "$rules_file" ]; then
564
641
  while IFS= read -r rule || [ -n "$rule" ]; do
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -558,7 +558,84 @@ if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(tabl
558
558
  block "destructive SQL (DROP/TRUNCATE) detected"
559
559
  fi
560
560
 
561
- # 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
561
+ # 14. The git control plane. A recursive forced delete of `.git` destroys every
562
+ # commit, branch and stash that is not already pushed — the one directory whose
563
+ # loss is not recoverable from the working tree.
564
+ #
565
+ # This is not a new principle, it is a hole in one the guard already holds. Rule
566
+ # 4 above already refuses `rm -rf "$SOMEDIR"` and paths outside the project;
567
+ # `rm -rf .git` was permitted only because nothing named it. Measured before
568
+ # this rule existed: `rm -rf .git`, `rm -rf ./.git` and `rm -rf .git/objects`
569
+ # were all ALLOWED while the variable-expanded case was correctly blocked.
570
+ #
571
+ # `.git` must be a whole path component. `.gitignore`, `.gitattributes`,
572
+ # `.github/` and a `.git-old` backup are ordinary files a project deletes on
573
+ # purpose, and blocking those would be the collision that gets a guard switched
574
+ # off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
575
+ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
576
+ if matches_cs "$RM_RF_CLUSTER" || matches_cs "$RM_RF_SPLIT"; then
577
+ if matches_cs "$GIT_CONTROL_PLANE"; then
578
+ block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
579
+ fi
580
+ fi
581
+
582
+ # 15. Credential stores. Reading one into an agent transcript is disclosure even
583
+ # when nothing is copied anywhere: the value lands in a log, a context window,
584
+ # and whatever retains either.
585
+ #
586
+ # Scoped deliberately rather than by the obvious pattern. Each clause below is a
587
+ # separate judgement about a family where an agent has no legitimate read, and
588
+ # each carries the exclusions that keep ordinary work moving — public keys,
589
+ # known_hosts, ssh config, and `.env.example` are all still readable, because a
590
+ # guard that blocks routine setup is a guard somebody disables.
591
+
592
+ # The verbs that disclose: read it, duplicate it, or send it. Writing is NOT
593
+ # here — `echo FOO=1 > .env` during setup creates a secret rather than leaking
594
+ # one, and refusing it would block the very workflow that produces the file.
595
+ readonly SECRET_READ_VERB='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(cat|bat|less|more|head|tail|strings|xxd|od|base64|cp|mv|rsync|scp|sftp|tar|zip|curl|wget|http|nc)([[:space:]]|$)'
596
+
597
+ # 15a. SSH PRIVATE keys. `id_rsa.pub` and friends are public by construction and
598
+ # stay readable; so do `known_hosts`, `config` and `authorized_keys`, which
599
+ # agents legitimately inspect when diagnosing a remote.
600
+ readonly SSH_PRIVATE_KEY='\.ssh/(id_[[:alnum:]_-]+|[[:alnum:]_.-]*_(rsa|dsa|ecdsa|ed25519))([[:space:]'"$qc"']|$)'
601
+ # 15b. Cloud provider credential files.
602
+ readonly CLOUD_CREDENTIAL='(\.aws/(credentials|config)|\.config/gcloud/[[:alnum:]_./-]*credentials[[:alnum:]_.-]*|\.azure/(accessTokens|msal_token_cache)[[:alnum:]_.-]*|\.kube/config)([[:space:]'"$qc"']|$)'
603
+ # 15c. Coding-agent credential stores — the tokens that impersonate the operator
604
+ # to every service their agent can reach.
605
+ readonly AGENT_CREDENTIAL='(\.(claude|codex|cursor|copilot|gemini|antigravity)/[[:alnum:]_./-]*(credentials|auth|token)[[:alnum:]_.-]*|\.netrc|\.npmrc|\.pypirc|\.docker/config\.json)([[:space:]'"$qc"']|$)'
606
+ # 15d. Dotenv files. `.env.example`, `.sample`, `.template`, `.dist` and
607
+ # `.schema` are checked-in documentation of which keys exist, never the values,
608
+ # so they are excluded — that collision is the one this rule most had to avoid.
609
+ readonly DOTENV_SECRET='(^|[[:space:]='"$qc"'./])\.env(\.[[:alnum:]_-]+)?([[:space:]'"$qc"']|$)'
610
+ readonly DOTENV_PUBLIC='\.env\.(example|sample|template|dist|schema|defaults)([[:space:]'"$qc"']|$)'
611
+
612
+ if matches "$SECRET_READ_VERB"; then
613
+ if matches "$SSH_PRIVATE_KEY"; then
614
+ block "reading, copying or transmitting an SSH PRIVATE key. Public keys (.pub), known_hosts and ssh config are unaffected; if you need the fingerprint use \`ssh-keygen -lf <key>.pub\`."
615
+ fi
616
+ if matches "$CLOUD_CREDENTIAL"; then
617
+ block "reading, copying or transmitting a cloud provider credential file. Use the provider CLI's own identity command (\`aws sts get-caller-identity\`, \`gcloud auth list\`) instead of reading the file."
618
+ fi
619
+ if matches "$AGENT_CREDENTIAL"; then
620
+ block "reading, copying or transmitting a coding-agent or package-registry credential store. These tokens impersonate the operator to every service the agent can reach."
621
+ fi
622
+ # `.env` only when the path is NOT one of the public example forms. Ordered as
623
+ # an exclusion rather than a narrower pattern so a new example suffix is one
624
+ # word to add, and so the reason a file is exempt stays readable.
625
+ if matches "$DOTENV_SECRET" && ! matches "$DOTENV_PUBLIC"; then
626
+ block "reading, copying or transmitting a dotenv file holding real values. \`.env.example\`, \`.env.sample\`, \`.env.template\` and \`.env.dist\` are readable, and WRITING a .env is unaffected — only reading one back is refused."
627
+ fi
628
+ # Fail-closed, narrowly. A read whose DIRECTORY is a known credential store
629
+ # but whose leaf is variable-expanded cannot be classified, and #2980's lesson
630
+ # is that a guard which cannot classify must refuse rather than permit. Scoped
631
+ # to credential directories on purpose: refusing every \`cat "\$FILE"\` would be
632
+ # the over-reach that gets the whole hook switched off.
633
+ if matches_cs '(\.ssh|\.aws|\.config/gcloud|\.azure|\.kube)/[^[:space:]'"$qc"']*\$'; then
634
+ block "reading a path inside a credential directory whose filename is variable-expanded, so the guard cannot tell which file it is. Name the file explicitly if it is genuinely not a secret."
635
+ fi
636
+ fi
637
+
638
+ # 16. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
562
639
  rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
563
640
  if [ -f "$rules_file" ]; then
564
641
  while IFS= read -r rule || [ -n "$rule" ]; do
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.4.8",
3
+ "version": "4.4.10",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"