ghostrules 0.2.1__tar.gz → 0.3.0__tar.gz

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 (223) hide show
  1. {ghostrules-0.2.1 → ghostrules-0.3.0}/.ghostrules.lock +1 -1
  2. {ghostrules-0.2.1 → ghostrules-0.3.0}/.ghostrules.toml +2 -0
  3. ghostrules-0.3.0/.github/workflows/corpus.yml +79 -0
  4. ghostrules-0.3.0/.github/workflows/pages.yml +48 -0
  5. {ghostrules-0.2.1 → ghostrules-0.3.0}/.gitignore +6 -0
  6. {ghostrules-0.2.1 → ghostrules-0.3.0}/CHANGELOG.md +53 -0
  7. {ghostrules-0.2.1 → ghostrules-0.3.0}/PKG-INFO +26 -3
  8. {ghostrules-0.2.1 → ghostrules-0.3.0}/README.md +20 -2
  9. {ghostrules-0.2.1 → ghostrules-0.3.0}/action.yml +2 -4
  10. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/DECISIONS.md +147 -0
  11. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/RULES.md +49 -9
  12. {ghostrules-0.2.1 → ghostrules-0.3.0}/pyproject.toml +9 -0
  13. ghostrules-0.3.0/release-notes.md +50 -0
  14. ghostrules-0.3.0/scripts/build_site.py +41 -0
  15. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/__init__.py +1 -1
  16. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/imports.py +3 -0
  17. ghostrules-0.3.0/src/ghostrules/playground.py +151 -0
  18. ghostrules-0.3.0/src/ghostrules/reporters/context.py +161 -0
  19. ghostrules-0.2.1/src/ghostrules/reporters/context.py → ghostrules-0.3.0/src/ghostrules/reporters/context_data.py +3 -147
  20. ghostrules-0.3.0/src/ghostrules/reveal.py +37 -0
  21. ghostrules-0.3.0/src/ghostrules/reveal_data.py +107 -0
  22. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/injection.py +200 -15
  23. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/patterns.py +29 -9
  24. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/unicode.py +23 -2
  25. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/sensitive.py +3 -1
  26. ghostrules-0.3.0/study/analyze.py +278 -0
  27. ghostrules-0.3.0/study/charts/01-adoption-dark.svg +245 -0
  28. ghostrules-0.3.0/study/charts/01-adoption-light.svg +245 -0
  29. ghostrules-0.3.0/study/charts/02-tools-per-repo-dark.svg +143 -0
  30. ghostrules-0.3.0/study/charts/02-tools-per-repo-light.svg +143 -0
  31. ghostrules-0.3.0/study/charts/03-claude-agents-dark.svg +126 -0
  32. ghostrules-0.3.0/study/charts/03-claude-agents-light.svg +126 -0
  33. ghostrules-0.3.0/study/charts/04-what-agents-read-dark.svg +143 -0
  34. ghostrules-0.3.0/study/charts/04-what-agents-read-light.svg +143 -0
  35. ghostrules-0.3.0/study/charts/05-risk-dark.svg +126 -0
  36. ghostrules-0.3.0/study/charts/05-risk-light.svg +126 -0
  37. ghostrules-0.3.0/study/charts/06-precision-dark.svg +432 -0
  38. ghostrules-0.3.0/study/charts/06-precision-light.svg +432 -0
  39. ghostrules-0.3.0/study/charts.py +283 -0
  40. ghostrules-0.3.0/study/data/adoption_families.csv +13 -0
  41. ghostrules-0.3.0/study/data/claude_agents_relation.csv +5 -0
  42. ghostrules-0.3.0/study/data/conditional_rules.csv +5 -0
  43. ghostrules-0.3.0/study/data/findings_by_rule.csv +33 -0
  44. ghostrules-0.3.0/study/data/headline.json +18 -0
  45. ghostrules-0.3.0/study/data/imports.csv +6 -0
  46. ghostrules-0.3.0/study/data/instruction_file_lines.csv +6 -0
  47. ghostrules-0.3.0/study/data/numbers.csv +12 -0
  48. ghostrules-0.3.0/study/data/precision_by_rule.csv +15 -0
  49. ghostrules-0.3.0/study/data/precision_runs.csv +5 -0
  50. ghostrules-0.3.0/study/data/risk_indicators.csv +7 -0
  51. ghostrules-0.3.0/study/data/sample.csv +3 -0
  52. ghostrules-0.3.0/study/data/skills_concentration.csv +7 -0
  53. ghostrules-0.3.0/study/data/tools_per_repo.csv +8 -0
  54. ghostrules-0.3.0/study/features.py +138 -0
  55. ghostrules-0.3.0/study/label_classes.py +133 -0
  56. ghostrules-0.3.0/study/labels.toml +2646 -0
  57. ghostrules-0.3.0/study/report.md +166 -0
  58. ghostrules-0.3.0/study/report.py +211 -0
  59. ghostrules-0.3.0/tests/corpus_fakes.py +103 -0
  60. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_ci_files.py +22 -2
  61. ghostrules-0.3.0/tests/test_core_without_cli_deps.py +74 -0
  62. ghostrules-0.3.0/tests/test_corpus_packaging.py +31 -0
  63. ghostrules-0.3.0/tests/test_corpus_tools.py +634 -0
  64. ghostrules-0.3.0/tests/test_playground.py +62 -0
  65. ghostrules-0.3.0/tests/test_precision_study.py +164 -0
  66. ghostrules-0.3.0/tests/test_precision_v030.py +343 -0
  67. ghostrules-0.3.0/tests/test_study.py +138 -0
  68. ghostrules-0.3.0/tests/web/test_playground_smoke.py +99 -0
  69. ghostrules-0.3.0/web/app.js +376 -0
  70. ghostrules-0.3.0/web/index.html +77 -0
  71. ghostrules-0.3.0/web/style.css +101 -0
  72. ghostrules-0.2.1/release-notes.md +0 -43
  73. ghostrules-0.2.1/src/ghostrules/reveal.py +0 -90
  74. {ghostrules-0.2.1 → ghostrules-0.3.0}/.gitattributes +0 -0
  75. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  76. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/false-positive.yml +0 -0
  77. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/new-rule.yml +0 -0
  78. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/ci.yml +0 -0
  79. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/release.yml +0 -0
  80. {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/self-test.yml +0 -0
  81. {ghostrules-0.2.1 → ghostrules-0.3.0}/.pre-commit-hooks.yaml +0 -0
  82. {ghostrules-0.2.1 → ghostrules-0.3.0}/CLAUDE.md +0 -0
  83. {ghostrules-0.2.1 → ghostrules-0.3.0}/CONTRIBUTING.md +0 -0
  84. {ghostrules-0.2.1 → ghostrules-0.3.0}/LICENSE +0 -0
  85. {ghostrules-0.2.1 → ghostrules-0.3.0}/SECURITY.md +0 -0
  86. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/README.md +0 -0
  87. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/demo.gif +0 -0
  88. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/demo.tape +0 -0
  89. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/imports.gif +0 -0
  90. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/imports.tape +0 -0
  91. {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/lock.tape +0 -0
  92. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/AGENT_MODELS.md +0 -0
  93. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/PLAN.md +0 -0
  94. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/SPEC.md +0 -0
  95. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.1.1/GR012-FIX.md +0 -0
  96. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/PLAN.md +0 -0
  97. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/RULES.md +0 -0
  98. {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/SPEC.md +0 -0
  99. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/.claude/settings.json +0 -0
  100. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/.mcp.json +0 -0
  101. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/AGENTS.md +0 -0
  102. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/CLAUDE.md +0 -0
  103. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/README.md +0 -0
  104. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/.claude/rules/api.md +0 -0
  105. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/.env +0 -0
  106. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/AGENTS.md +0 -0
  107. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/CLAUDE.md +0 -0
  108. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/README.md +0 -0
  109. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/docs/setup.md +0 -0
  110. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/docs/style.md +0 -0
  111. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/shared/team-rules.md +0 -0
  112. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/src/api/users.ts +0 -0
  113. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/src/legacy/CLAUDE.md +0 -0
  114. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.claude/settings.json +0 -0
  115. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.claude/skills/setup/SKILL.md +0 -0
  116. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.cursorrules +0 -0
  117. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.github/copilot-instructions.md +0 -0
  118. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.mcp.json +0 -0
  119. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/AGENTS.md +0 -0
  120. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/CLAUDE.md +0 -0
  121. {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/README.md +0 -0
  122. {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/changelog_section.py +0 -0
  123. {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/gen_rules_table.py +0 -0
  124. {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/make_demo.py +0 -0
  125. {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/make_fixtures.py +0 -0
  126. {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/set_owner.py +0 -0
  127. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/_compat.py +0 -0
  128. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/__init__.py +0 -0
  129. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/base.py +0 -0
  130. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/claude_code.py +0 -0
  131. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/copilot_cli.py +0 -0
  132. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/cursor.py +0 -0
  133. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/generic.py +0 -0
  134. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/repo.py +0 -0
  135. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/clean.py +0 -0
  136. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/cli.py +0 -0
  137. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/config.py +0 -0
  138. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/context_command.py +0 -0
  139. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/contextscan.py +0 -0
  140. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/globs.py +0 -0
  141. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/jsonloc.py +0 -0
  142. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/lock.py +0 -0
  143. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/markdown_utils.py +0 -0
  144. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/mcp_config.py +0 -0
  145. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/models.py +0 -0
  146. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/py.typed +0 -0
  147. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/__init__.py +0 -0
  148. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/common.py +0 -0
  149. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/json_.py +0 -0
  150. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/lock.py +0 -0
  151. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/markdown.py +0 -0
  152. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/sarif.py +0 -0
  153. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/table.py +0 -0
  154. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/__init__.py +0 -0
  155. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/agent_settings.py +0 -0
  156. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/context_rules.py +0 -0
  157. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/directive.py +0 -0
  158. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/helpers.py +0 -0
  159. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/known_installers.py +0 -0
  160. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/mcp.py +0 -0
  161. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/registry.py +0 -0
  162. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/scancontext.py +0 -0
  163. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/scanner.py +0 -0
  164. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/secrets.py +0 -0
  165. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/targets.py +0 -0
  166. {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/unicode_utils.py +0 -0
  167. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/conftest.py +0 -0
  168. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/data/sarif-schema-2.1.0.json +0 -0
  169. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_flag_forged/AGENTS.md +0 -0
  170. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_flag_ok/AGENTS.md +0 -0
  171. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_tags/AGENTS.md +0 -0
  172. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr002_bidi/AGENTS.md +0 -0
  173. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr002_rtl_medium/AGENTS.md +0 -0
  174. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_bom_midfile/AGENTS.md +0 -0
  175. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_bom_ok/AGENTS.md +0 -0
  176. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_emoji_zwj_ok/AGENTS.md +0 -0
  177. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_indic_ok/AGENTS.md +0 -0
  178. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_persian_ok/AGENTS.md +0 -0
  179. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_soft_hyphen/AGENTS.md +0 -0
  180. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_file_high/AGENTS.md +0 -0
  181. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_line_high/AGENTS.md +0 -0
  182. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_medium/AGENTS.md +0 -0
  183. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr004_vs_bytes/AGENTS.md +0 -0
  184. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr004_vs_ok/AGENTS.md +0 -0
  185. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_benign_ok/CLAUDE.md +0 -0
  186. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_imperative/CLAUDE.md +0 -0
  187. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_injection/CLAUDE.md +0 -0
  188. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_multiline/CLAUDE.md +0 -0
  189. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_in_code_block_ok/CLAUDE.md +0 -0
  190. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_short_ok/CLAUDE.md +0 -0
  191. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_unterminated/CLAUDE.md +0 -0
  192. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_ok/AGENTS.md +0 -0
  193. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_url/AGENTS.md +0 -0
  194. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_word/AGENTS.md +0 -0
  195. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/secret_samples.py +0 -0
  196. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_claude.py +0 -0
  197. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_others.py +0 -0
  198. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_repo.py +0 -0
  199. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_cli.py +0 -0
  200. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_config.py +0 -0
  201. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_context_command.py +0 -0
  202. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_demo.py +0 -0
  203. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_directive.py +0 -0
  204. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_docs.py +0 -0
  205. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_globs.py +0 -0
  206. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_imports.py +0 -0
  207. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_lock.py +0 -0
  208. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_markdown_utils.py +0 -0
  209. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_mcp_config.py +0 -0
  210. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_models.py +0 -0
  211. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_patterns.py +0 -0
  212. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_precision_v021.py +0 -0
  213. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_reporters.py +0 -0
  214. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_config.py +0 -0
  215. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_injection.py +0 -0
  216. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_unicode.py +0 -0
  217. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_scan_imports.py +0 -0
  218. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_scanner.py +0 -0
  219. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_secret_masking.py +0 -0
  220. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_secrets.py +0 -0
  221. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_symlink_alias.py +0 -0
  222. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_targets.py +0 -0
  223. {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_unicode_utils.py +0 -0
@@ -26,6 +26,6 @@
26
26
  "generic": []
27
27
  },
28
28
  "configs": [],
29
- "generator": "ghostrules 0.2.1",
29
+ "generator": "ghostrules 0.3.0",
30
30
  "lock_version": 1
31
31
  }
@@ -7,4 +7,6 @@ exclude = [
7
7
  ".mypy_cache/**",
8
8
  ".ruff_cache/**",
9
9
  ".pytest_cache/**",
10
+ "corpus/.cache/**", # API responses and clones are other people's text
11
+ "corpus/results/**",
10
12
  ]
@@ -0,0 +1,79 @@
1
+ name: Corpus
2
+
3
+ # Runs ghostrules against the public repositories listed in corpus/repos.toml (the only networked
4
+ # part of the project). Never on pull requests.
5
+ on:
6
+ schedule:
7
+ - cron: "17 4 * * 1"
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ concurrency:
14
+ group: corpus
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ corpus:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 120
21
+ env:
22
+ GH_TOKEN: ${{ github.token }}
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+ - name: Install ghostrules from this checkout
29
+ run: pip install -e ".[dev]"
30
+ - name: Scan the regression tier
31
+ run: python corpus/run.py --tier regression
32
+ - name: Gate against labels.toml
33
+ run: python corpus/check.py
34
+ - name: Aggregate files (badge and summary, counts only)
35
+ if: always() && hashFiles('corpus/results/latest-summary.json') != ''
36
+ run: python corpus/badge.py --out corpus-public
37
+ - name: Upload results
38
+ if: always()
39
+ uses: actions/upload-artifact@v4
40
+ with:
41
+ name: corpus-results
42
+ path: |
43
+ corpus/results
44
+ corpus-public
45
+ retention-days: 30
46
+ if-no-files-found: ignore
47
+
48
+ publish:
49
+ # Only the default branch publishes; the badge reads corpus/badge.json from gh-pages.
50
+ needs: corpus
51
+ if: always() && needs.corpus.result != 'skipped' && github.ref == 'refs/heads/main'
52
+ runs-on: ubuntu-latest
53
+ permissions:
54
+ contents: write
55
+ steps:
56
+ - uses: actions/download-artifact@v4
57
+ with:
58
+ name: corpus-results
59
+ path: artifact
60
+ - name: Publish to gh-pages
61
+ env:
62
+ GH_TOKEN: ${{ github.token }}
63
+ REPO: ${{ github.repository }}
64
+ run: |
65
+ set -euo pipefail
66
+ test -f artifact/corpus-public/badge.json
67
+ git clone --quiet --depth 1 --branch gh-pages \
68
+ "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" site \
69
+ || { mkdir site && git -C site init --quiet -b gh-pages \
70
+ && git -C site remote add origin \
71
+ "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"; }
72
+ mkdir -p site/corpus
73
+ cp artifact/corpus-public/badge.json artifact/corpus-public/summary.json site/corpus/
74
+ cd site
75
+ git config user.name "github-actions[bot]"
76
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
77
+ git add corpus
78
+ git diff --cached --quiet || git commit --quiet -m "corpus: weekly summary"
79
+ git push origin gh-pages
@@ -0,0 +1,48 @@
1
+ name: Playground
2
+
3
+ # Builds the browser playground (web/ plus the wheel of this checkout) and deploys it to GitHub Pages.
4
+ on:
5
+ push:
6
+ tags: ["v*"]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: pages
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
21
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
22
+ with:
23
+ python-version: "3.12"
24
+ - name: Install
25
+ run: pip install -e ".[dev,web-test]"
26
+ - name: Build the site
27
+ run: python scripts/build_site.py _site
28
+ - name: Playwright smoke test on the built site
29
+ run: |
30
+ playwright install --with-deps chromium
31
+ pytest -q tests/web
32
+ - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
33
+ - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
34
+ with:
35
+ path: _site
36
+
37
+ deploy:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ pages: write
42
+ id-token: write
43
+ environment:
44
+ name: github-pages
45
+ url: ${{ steps.deployment.outputs.page_url }}
46
+ steps:
47
+ - id: deployment
48
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
@@ -12,3 +12,9 @@ dist/
12
12
  htmlcov/
13
13
  *.ghostrules.bak
14
14
  ghostrules.sarif
15
+
16
+ # corpus tooling
17
+ corpus/.cache/
18
+ corpus/results/*
19
+ !corpus/results/latest-summary.json
20
+ _site/
@@ -5,6 +5,59 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.3.0] - 2026-10-28
9
+
10
+ Measurement release. ghostrules now runs against real repositories every week, a browser playground
11
+ runs the real scanner without installing anything, and the first full study of popular repositories
12
+ drove a round of precision fixes. On the 300-repository regression tier 0.2.1 reported 323 findings
13
+ (84 critical, 156 high); 0.3.0 reports 212 (0 critical, 53 high), every critical and high finding
14
+ reviewed. On the 1,283-repository study tier the first run reported 1,072 findings (13 critical, 296
15
+ high); after the fixes 945 (12 critical, 252 high), all of medium and above triaged.
16
+
17
+ ### Added
18
+ - **Playground** (`web/`, `.github/workflows/pages.yml`): a static page that runs the real scanner in
19
+ the browser with Pyodide. Paste a file, drop a folder (agent files and what they import, up to
20
+ 5 MB) or name a public GitHub repo (the only tab that uses the network). Shows what you see versus
21
+ what the agent reads, the context tree per agent and downloadable JSON, Markdown and SARIF reports.
22
+ Playwright smoke test; deployed to GitHub Pages on release tags.
23
+ - **Corpus tooling** (`corpus/`, outside the package and the wheel): `discover.py`, `run.py`,
24
+ `check.py`, `triage.py`, `badge.py`, `filestats.py`, a weekly `corpus.yml` workflow and an endpoint
25
+ badge in the README. Findings are labelled by fingerprint in `corpus/labels.toml`; labels never
26
+ name a repository for a reported finding.
27
+ - **Study tooling** (`study/`): per-repository features, aggregate CSVs, SVG charts in light and dark,
28
+ the triage classes and `study/report.md`, generated from the CSVs.
29
+ - `ghostrules.playground`, `ghostrules.reveal_data` and `ghostrules.reporters.context_data`: the core
30
+ (scanner, rules, context engine, JSON, SARIF and Markdown reporters) now imports without `typer`
31
+ and `rich`; the terminal output is unchanged.
32
+ - `action.yml` is named `ghostrules scan` and its description fits the Marketplace limit (125
33
+ characters).
34
+
35
+ ### Fixed
36
+ - GR003: a lone U+200B between two letters of Khmer, Thai, Lao or Myanmar text is that script's word
37
+ separator and is ignored.
38
+ - GR005: a hidden comment is `medium` when it carries an instruction cue, `low` when it only mentions
39
+ agents, `high` unchanged for injection matches.
40
+ - GR010: `override system|safety` needs a following noun, and defensive text that quotes the phrase
41
+ is `low`.
42
+ - GR011: `without asking the user` is no longer concealment; a prohibition earlier in the clause, a
43
+ safety claim ("does not silently install"), a description in the third person ("silently runs") and
44
+ a modal before it are not reported.
45
+ - GR012: naming the `curl | sh` pattern without a target is `low`; a `curl` in prose no longer joins
46
+ the next code block; `| bash script.sh` is not a data pipe into the shell.
47
+ - GR013: needs a verb, a secret and a URL in the same clause; prohibitions, loopback URLs, `POST`
48
+ and `.post(`, "send the user to" and "send X only to the fixed origin" are not exfiltration.
49
+ - GR014: the verb must govern the path across no paragraph break, clause connector (`before`,
50
+ `after`, `when`), passive or noun use; `### Output` is a heading; `.env.default` and copying a
51
+ dotenv file are not reads.
52
+ - GR015: runs inside URLs, Subresource Integrity digests and low-entropy hex (`AAAA…`) are skipped.
53
+ - GR016: a prohibition ("Avoid `--yolo`"), a table row and a list item that documents a flag are not
54
+ an instruction.
55
+ - A bare `@/`, `@~/`, `@./` or `@../` is a path prefix, not an import.
56
+
57
+ ### Known limits
58
+ - Splitting an exfiltration instruction across two sentences avoids the GR013 prose match, and a
59
+ concealment right after an unrelated prohibition in the same clause is not reported.
60
+
8
61
  ## [0.2.1] - 2026-10-28
9
62
 
10
63
  Precision release. A scan of 12 popular public repositories with 0.2.0 found no critical issue,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ghostrules
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Find the instructions your AI agent sees but you don't.
5
5
  Project-URL: Homepage, https://github.com/BardLaudian/ghostrules
6
6
  Project-URL: Documentation, https://github.com/BardLaudian/ghostrules#readme
@@ -45,21 +45,29 @@ Requires-Dist: rich>=13
45
45
  Requires-Dist: tomli>=2.0; python_version < '3.11'
46
46
  Requires-Dist: typer>=0.12
47
47
  Provides-Extra: dev
48
+ Requires-Dist: hatchling; extra == 'dev'
48
49
  Requires-Dist: jsonschema>=4; extra == 'dev'
49
50
  Requires-Dist: mypy>=1.10; extra == 'dev'
50
51
  Requires-Dist: pytest-cov>=5; extra == 'dev'
51
52
  Requires-Dist: pytest>=8; extra == 'dev'
52
53
  Requires-Dist: pyyaml>=6; extra == 'dev'
53
54
  Requires-Dist: ruff>=0.6; extra == 'dev'
55
+ Provides-Extra: study
56
+ Requires-Dist: matplotlib>=3.8; extra == 'study'
57
+ Provides-Extra: web-test
58
+ Requires-Dist: playwright>=1.45; extra == 'web-test'
54
59
  Description-Content-Type: text/markdown
55
60
 
56
61
  # ghostrules
57
62
 
58
63
  *Know exactly what your AI agent reads — and approve every change to it.*
59
64
 
65
+ **[Try it in your browser →](https://bardlaudian.github.io/ghostrules/)** Paste an `AGENTS.md`, drop a folder or name a public repo. It runs the real scanner in the page: your files never leave it.
66
+
60
67
  [![CI](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml/badge.svg)](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml)
61
68
  [![PyPI](https://img.shields.io/pypi/v/ghostrules.svg)](https://pypi.org/project/ghostrules/)
62
69
  [![Python versions](https://img.shields.io/pypi/pyversions/ghostrules.svg)](https://pypi.org/project/ghostrules/)
70
+ [![Corpus](https://img.shields.io/endpoint?url=https%3A%2F%2Fbardlaudian.github.io%2Fghostrules%2Fcorpus%2Fbadge.json)](https://github.com/BardLaudian/ghostrules/blob/main/corpus/README.md)
63
71
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/BardLaudian/ghostrules/blob/main/LICENSE)
64
72
 
65
73
  ![A CLAUDE.md that looks clean imports a file that imports a file with a hidden instruction; ghostrules finds it and shows the chain](https://raw.githubusercontent.com/BardLaudian/ghostrules/main/demo/imports.gif)
@@ -352,7 +360,7 @@ jobs:
352
360
  runs-on: ubuntu-latest
353
361
  steps:
354
362
  - uses: actions/checkout@v4
355
- - uses: BardLaudian/ghostrules@v0.2.1
363
+ - uses: BardLaudian/ghostrules@v0.3.0
356
364
  with:
357
365
  fail-on: high # critical | high | medium | low | info | none
358
366
  all-files: "false" # also check every text file for hidden Unicode
@@ -385,7 +393,7 @@ output `lock-exit-code` is `0` up to date, `1` changed, `2` the check could not
385
393
  ```yaml
386
394
  repos:
387
395
  - repo: https://github.com/BardLaudian/ghostrules
388
- rev: v0.2.1
396
+ rev: v0.3.0
389
397
  hooks:
390
398
  - id: ghostrules
391
399
  ```
@@ -545,6 +553,21 @@ the list.
545
553
  Findings below that level are still reported but do not change the exit code, and `--fail-on none`
546
554
  never fails.
547
555
 
556
+ ## How we test precision
557
+
558
+ A scanner that cries wolf gets switched off, so precision is measured, not assumed. Every week a
559
+ workflow scans 300 popular public repositories with agent files (`corpus/`, the only part of the
560
+ project that uses the network) and fails if a critical or high finding has not been reviewed. Each
561
+ false positive found that way became a test and a rule change; findings that are correct matches in
562
+ a harmless context (a `curl | sh` installer in a README) are labelled and kept. On the first run of
563
+ the 300 repositories 0.2.1 reported 84 critical and 156 high findings; 0.3.0 reports none critical
564
+ and 53 high, all reviewed.
565
+
566
+ The same tooling scanned 1,583 repositories for
567
+ [a study of what popular repositories tell their AI agents](https://github.com/BardLaudian/ghostrules/blob/main/study/report.md):
568
+ aggregates only, no repository is named next to a finding, and every number comes from the CSVs in
569
+ `study/data/`.
570
+
548
571
  ## Limits
549
572
 
550
573
  `ghostrules` is a set of heuristics, not a proof: no findings does not mean a file is safe, and some
@@ -2,9 +2,12 @@
2
2
 
3
3
  *Know exactly what your AI agent reads — and approve every change to it.*
4
4
 
5
+ **[Try it in your browser →](https://bardlaudian.github.io/ghostrules/)** Paste an `AGENTS.md`, drop a folder or name a public repo. It runs the real scanner in the page: your files never leave it.
6
+
5
7
  [![CI](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml/badge.svg)](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml)
6
8
  [![PyPI](https://img.shields.io/pypi/v/ghostrules.svg)](https://pypi.org/project/ghostrules/)
7
9
  [![Python versions](https://img.shields.io/pypi/pyversions/ghostrules.svg)](https://pypi.org/project/ghostrules/)
10
+ [![Corpus](https://img.shields.io/endpoint?url=https%3A%2F%2Fbardlaudian.github.io%2Fghostrules%2Fcorpus%2Fbadge.json)](https://github.com/BardLaudian/ghostrules/blob/main/corpus/README.md)
8
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/BardLaudian/ghostrules/blob/main/LICENSE)
9
12
 
10
13
  ![A CLAUDE.md that looks clean imports a file that imports a file with a hidden instruction; ghostrules finds it and shows the chain](https://raw.githubusercontent.com/BardLaudian/ghostrules/main/demo/imports.gif)
@@ -297,7 +300,7 @@ jobs:
297
300
  runs-on: ubuntu-latest
298
301
  steps:
299
302
  - uses: actions/checkout@v4
300
- - uses: BardLaudian/ghostrules@v0.2.1
303
+ - uses: BardLaudian/ghostrules@v0.3.0
301
304
  with:
302
305
  fail-on: high # critical | high | medium | low | info | none
303
306
  all-files: "false" # also check every text file for hidden Unicode
@@ -330,7 +333,7 @@ output `lock-exit-code` is `0` up to date, `1` changed, `2` the check could not
330
333
  ```yaml
331
334
  repos:
332
335
  - repo: https://github.com/BardLaudian/ghostrules
333
- rev: v0.2.1
336
+ rev: v0.3.0
334
337
  hooks:
335
338
  - id: ghostrules
336
339
  ```
@@ -490,6 +493,21 @@ the list.
490
493
  Findings below that level are still reported but do not change the exit code, and `--fail-on none`
491
494
  never fails.
492
495
 
496
+ ## How we test precision
497
+
498
+ A scanner that cries wolf gets switched off, so precision is measured, not assumed. Every week a
499
+ workflow scans 300 popular public repositories with agent files (`corpus/`, the only part of the
500
+ project that uses the network) and fails if a critical or high finding has not been reviewed. Each
501
+ false positive found that way became a test and a rule change; findings that are correct matches in
502
+ a harmless context (a `curl | sh` installer in a README) are labelled and kept. On the first run of
503
+ the 300 repositories 0.2.1 reported 84 critical and 156 high findings; 0.3.0 reports none critical
504
+ and 53 high, all reviewed.
505
+
506
+ The same tooling scanned 1,583 repositories for
507
+ [a study of what popular repositories tell their AI agents](https://github.com/BardLaudian/ghostrules/blob/main/study/report.md):
508
+ aggregates only, no repository is named next to a finding, and every number comes from the CSVs in
509
+ `study/data/`.
510
+
493
511
  ## Limits
494
512
 
495
513
  `ghostrules` is a set of heuristics, not a proof: no findings does not mean a file is safe, and some
@@ -1,7 +1,5 @@
1
- name: ghostrules
2
- description: >-
3
- Find hidden instructions and risky configuration in the files AI coding agents read and obey
4
- (AGENTS.md, CLAUDE.md, .cursorrules, skills, MCP configs...). Read-only: nothing is executed.
1
+ name: ghostrules scan
2
+ description: Find hidden instructions and risky settings in AGENTS.md, CLAUDE.md, .cursorrules, skills and MCP configs.
5
3
  author: Pablo Seoane Temprano
6
4
  branding:
7
5
  icon: eye-off
@@ -742,3 +742,150 @@ out `low` (example), FIXES.md expects `medium`. Two changes:
742
742
  ..."), so the sentence is a description. A code span that is the whole clause, or is followed by a
743
743
  verb or "and", stays a directive (`` `cat ~/.ssh/id_rsa` `` is still high).
744
744
  Corpus after the change: n8n l.649 is `medium`; the other three remaining findings are unchanged.
745
+
746
+ ## v0.3 Phase 1 — corpus tooling
747
+
748
+ - **Location and packaging.** `corpus/` is outside `src/`, so the wheel (which packages only
749
+ `src/ghostrules`) never contains it; the sdist excludes `/corpus` in `pyproject.toml`.
750
+ `tests/test_corpus_packaging.py` builds both with hatchling and checks the file lists (`hatchling`
751
+ is in the `dev` extra). The scripts import each other as top-level modules (`import common`), so they
752
+ run as `python corpus/x.py`; the tests add `corpus/` to `sys.path`.
753
+ - **No shell, one injectable layer.** Every `git`, `gh` and `ghostrules` call goes through a `Runner`
754
+ (list of arguments, never a shell); tests use `tests/corpus_fakes.FakeRunner`. Repository names are
755
+ validated before they reach any command (no leading `-` or `.`, so never an option).
756
+ - **Measurement is isolated.** `run.py` always passes `--config <empty file>` and runs from a directory
757
+ outside the clone, so a scanned repo's `.ghostrules.toml` cannot change the result. The command is
758
+ `python -I -c "from ghostrules.cli import app; app()"` because the package has no `__main__`.
759
+ - **Stored results.** No `message` or `fix_hint`; `snippet` is re-masked with `mask_secrets` and passed
760
+ through `safe_text` (repository content may carry terminal escapes); `decoded` only for GR001/GR004;
761
+ context only as counts and readable paths. `triage.py` re-fetches the file with `gh api` for display
762
+ only.
763
+ - **Labels and privacy.** `labels.toml` is committed, so `reported` and `tp` labels never name the
764
+ repository (only `fp` and `wontfix`, which are benign, do). Messages from `check.py` carry the
765
+ fingerprint, not the repository. An `fp` label must record its old severity and an existing test.
766
+ - **check.py exit codes.** 1 = unlabelled critical/high, `fp` at or above its old severity, `fp`
767
+ without an existing test. 2 = a scan, context run or timeout failed (or no run exists). A failed
768
+ clone is reported but is not a gate: repositories get renamed or deleted.
769
+ - **discover.py.** Responses (200/404/409/451) are cached forever in `corpus/.cache/` (`--refresh`
770
+ ignores it); a rate-limited reply sleeps until `X-RateLimit-Reset`. A truncated tree is `null`
771
+ ("unknown"). `--propose` picks round-robin across languages, most starred first, as a starting point
772
+ only: the human edits the list.
773
+ - **Real check (2026-10-28).** `run.py --repos getsentry/sentry-python,anthropics/claude-agent-sdk-python`:
774
+ both `ok`, 2 findings (low), `check.py` PASS.
775
+
776
+ ## v0.3 Phase 2 — first corpus run (2026-10-28, 300 repos, regression tier)
777
+ - **Selection.** `discover.py`, 10 languages x 250 most-starred candidates (>= 2,000 stars, pushed
778
+ within 6 months), 1,112 with agent files; the 300 are 30 per language, round-robin, approved by the
779
+ user. Biased to popular repositories by construction.
780
+ - **First run:** 323 findings, 240 critical/high in 37 repos, three repos produced 158 of them.
781
+ Nothing from GR001/GR004/GR015/GR101/GR021 at medium or above: no secret or hidden payload found.
782
+ - **GR013 (82 critical).** Causes: `POST` matched as a verb (`curl -X POST`, `requests.post(`); a
783
+ verb, a word like "token" and a URL anywhere within 80/200 characters of normalised text (it joined
784
+ list items and code); prohibitions ("Never send credentials ..."); loopback URLs; "send the user
785
+ to". Fixed with a same-clause requirement, the other exclusions above and the directive /
786
+ descriptive / example severity. Known evasion: splitting the verb and the URL into two sentences
787
+ avoids the *prose* match; hidden decoded text is exempt from that filter.
788
+ - **GR010 (67 high).** Defensive guidance quoting "ignore previous instructions". Now classified; and
789
+ `override system|safety` requires a following noun.
790
+ - **GR012.** `curl ... | sh` named as a pattern (block lists, "must be rejected") is `low`.
791
+ - **Disclosure:** none needed so far.
792
+ - **Approved by the user (2026-10-28), changes to the contract:** (A) GR011 no longer matches
793
+ `without asking the user` and ignores matches under a prohibition; (B) GR014: `.env.default` is a
794
+ template, a local `copy` of a dotenv file does not fire, a bold/code-wrapped prohibition counts;
795
+ (C) a GR016 match in a Markdown table row is at most medium (not applied to GR010, so a table row
796
+ cannot hide an override). Known evasion: a real concealment preceded by an unrelated "not ..." within
797
+ the 40 characters before it is not reported.
798
+ - **Approved by the user (2026-10-28), D and E:** (D) GR003 ignores a lone ZWSP between letters of
799
+ Khmer/Thai/Lao/Myanmar (found in the Khmer translation of an AGENTS.md); (E) GR005 is medium only
800
+ with `you must|always|never`, low when the comment only names the agent, high unchanged when it
801
+ matches GR010-GR016. Also fixed (bug, no contract change): GR012 folded a prose "curl" onto the
802
+ `curl ... | bash` of the next code block (spans that cross a code fence are dropped *before*
803
+ deduplication; dropping them afterwards would have lost the real command).
804
+ - **More GR012 data pipes (corpus run 5):** `curl … | bash scripts/extract.sh "$tag"` runs a script from
805
+ a file (an argument with a `/` or a `.sh` suffix) and is not matched, like `python script.py`;
806
+ `| bash`, `| sh -s -- …` and `| sudo sh` still are. A pattern named inside a Markdown table cell
807
+ with an escaped pipe (`curl ... \| bash`) is also a mention.
808
+
809
+ ## v0.3 Phase 3 — corpus CI and badge
810
+
811
+ - **Badge text:** `corpus · 300 repos · 0 critical`, not "0 false positives". What the gate proves is
812
+ that no critical/high finding is unreviewed, so the badge reports the critical count of the latest
813
+ run and turns red ("gate failing") when `check.py` would fail.
814
+ - **Published files:** `corpus/badge.py` writes `badge.json` and `summary.json` with counts only
815
+ (no repository names, paths, snippets or per-repository timings), so the public branch carries
816
+ nothing that identifies a repository or a finding. The full results stay in the workflow artifact
817
+ (30 days).
818
+ - **Publishing:** weekly `schedule` and `workflow_dispatch`, never on pull requests. The `publish`
819
+ job runs only on `refs/heads/main` and writes `corpus/badge.json` and `corpus/summary.json` to
820
+ `gh-pages` with the job's `GITHUB_TOKEN` (`contents: write` on that job only). The README badge
821
+ reads `https://bardlaudian.github.io/ghostrules/corpus/badge.json`, which needs GitHub Pages
822
+ enabled on `gh-pages`.
823
+
824
+ ## v0.3 Phase 4 — web playground
825
+
826
+ - **Core without typer/rich:** `reveal.py` and `reporters/context.py` mixed data and `rich` drawing.
827
+ The data moved to `reveal_data.py` and `reporters/context_data.py` (the old modules re-export), so
828
+ the core imports without `typer`/`rich` (tested in a subprocess with them blocked). The wheel
829
+ installs in Pyodide 0.27.7 with `micropip.install(..., deps=False)`; no in-memory filesystem
830
+ adapter was needed (Pyodide's emulated FS is enough).
831
+ - **Bridge:** `ghostrules.playground` takes `{path: text}`, writes it to a temporary directory,
832
+ scans it and returns plain data (findings, per-agent context tree, revealed lines, JSON/Markdown/
833
+ SARIF reports). Paths with `..`, absolute paths and NULs are refused; limits are 5 MB and 500 files.
834
+ - **Folder and GitHub loading:** the page lists paths first, asks Python which are agent files
835
+ (`agent_paths`, the target globs plus `.claude/rules/**/*.md`) and reads only those, then follows
836
+ `@imports` (relative to the importing file, up to 6 rounds, `imported_paths`). Files over 1 MB are
837
+ skipped.
838
+ - **Rendering:** all file content is inserted with `textContent`, never as HTML. "What you see"
839
+ drops invisible characters and HTML comments; "What the agent reads" shows them as tokens.
840
+ - **Deploy:** `pages.yml` (release tags and manual) builds the site with `scripts/build_site.py`,
841
+ runs the Playwright smoke test (`tests/web`, skipped when playwright is missing, GitHub calls
842
+ mocked) and deploys with SHA-pinned official actions. The demo's hidden text is generated in JS,
843
+ so no invisible characters are stored in the repository.
844
+
845
+ ## v0.3 Phase 5 — study tooling
846
+
847
+ - **Selection (approved):** 3 stars strata (500-1999, 2000-9999, 10000+) x the 10 languages of the
848
+ regression tier (Rust, JavaScript, C++, Java, Ruby, Go, C#, Python, TypeScript, PHP), the 150
849
+ most-starred candidates per cell, public, non-fork, non-archived, pushed within 6 months, with at
850
+ least one agent file; about 500 repos per stratum, round-robin across languages. The regression
851
+ repos are excluded from `study` and the report uses both tiers together.
852
+ - **`run.py` records more numbers:** per context entry its load kind, import depth, whether it is
853
+ conditional and which transforms apply; per file (`filestats.py`) kind (agent / imported), size,
854
+ lines, symlink and its in-repo target, a 16-hex content hash (to find duplicated files) and the
855
+ number of HTML comments. No text is stored. Symlinks are read with `lstat`/`readlink`, never followed.
856
+
857
+ - **Study precision fixes (2026-10-28, first study run, 1,283 repos).** Real false positives, fixed
858
+ with tests in `tests/test_precision_study.py`: GR011 (prohibition anywhere in the clause; bare verb
859
+ only; modal / "either" before `silently`), GR016 (prohibition in the clause; a list item that opens
860
+ with the flag documents it, so medium), GR013 ("send X only to the fixed/official/configured …
861
+ origin" restricts where data goes, so it is descriptive, not critical), GR015 (runs inside URLs, SRI
862
+ digests, `AAAA…` padding), imports (a bare `@/`, `@~/`, `@./`, `@../` is a prefix, not a file).
863
+ Known evasions: a real concealment preceded by a prohibition in the same clause is not reported;
864
+ "send it only to the official https://attacker.example" is medium, not critical.
865
+ - **Left as they are (labelled in triage, not rule bugs):** GR003 on characters that really are in
866
+ the text (word joiner before `mapbox://`, a ZWSP demonstration in a security skill), GR012 on known
867
+ installers (`curl … | sh`), GR014 "read the `.env`" instructions in a project's own file, GR107 allow
868
+ lists, GR105 `bash` wrappers, GR020 imports that really leave the repository.
869
+
870
+ - **Study triage (2026-10-28, 1,283 study + 300 regression repos).** First study run: 1,072 findings
871
+ (13 critical, 296 high, 194 medium); after the precision fixes above: 945 (12 critical, 252 high,
872
+ 119 medium). Every finding of medium or above was labelled by class (`study/label_classes.py`,
873
+ approved by the user): 129 tp and 254 wontfix findings, 0 fp, 0 unlabelled; `study/labels.toml` names
874
+ no repository or path. `wontfix` means the rule matched what it describes in a benign context
875
+ (an installer one-liner, an attack documented in a security skill, a flag named in a CLI guide).
876
+ Nothing resembling a real secret or real malicious content appeared in either tier (no GR001,
877
+ GR002, GR004, GR101 or GR021 finding), so there is nothing to disclose.
878
+
879
+ - **Study charts and report (Phase 5).** Charts are drawn with matplotlib as SVG, one light and one
880
+ dark file each (`study/charts/NAME-light.svg` / `-dark.svg`), and the report picks one with a
881
+ `<picture>` and `prefers-color-scheme`. Single-series charts use the accent for the point and gray
882
+ for the context (emphasis), every value is labelled and the same numbers are in the report's tables.
883
+ The palette is the documented default one (slot 1 accent, the gray ramp); the palette validator
884
+ script could not be run here (no Node.js), so no categorical palette beyond that documented slot is
885
+ used. `study/report.py` writes `study/report.md` from the CSVs and a test checks the committed report
886
+ equals what the CSVs produce, so every number in it is a CSV number. matplotlib is an optional
887
+ `study` extra, not a runtime dependency.
888
+ - **Triage claim wording.** The 475 findings of medium or above (both tiers) were triaged by class,
889
+ each class checked on its examples and approved by the maintainer; the report says "triaged by
890
+ class", not "read one by one", and notes that a `wontfix` is a judgement (GR016: a flag named in a
891
+ guide).
@@ -22,7 +22,8 @@ if the file contains substantial RTL script (Arabic/Hebrew) — legitimate use.
22
22
  U+200B, U+200C, U+200D, U+2060, U+2061–U+2064, U+180E, U+FEFF (not at file start), U+00AD.
23
23
  Severity high if ≥ 5 in one line or ≥ 20 in the file (likely binary-encoded payload), else medium.
24
24
  Ignore U+200D between emoji code points (emoji ZWJ sequences) and U+200C/U+200D next to
25
- Indic/Persian letters.
25
+ Indic/Persian letters. Since 0.3 a lone U+200B between two letters of Khmer, Thai, Lao or Myanmar is
26
+ the word separator of those scripts and is ignored (a run, or a Latin neighbour, is still reported).
26
27
 
27
28
  ### GR004 — Variation-selector steganography (high)
28
29
  Runs of ≥ 3 chars in U+FE00–U+FE0F or U+E0100–U+E01EF not directly following an emoji / CJK base.
@@ -33,7 +34,8 @@ Runs of ≥ 3 chars in U+FE00–U+FE0F or U+E0100–U+E01EF not directly followi
33
34
  `<!-- ... -->` blocks in markdown instruction files are invisible when rendered on GitHub.
34
35
  Report **only** if the comment body is ≥ 40 chars and either matches any GR010–GR016 pattern
35
36
  (→ high, include which) or contains imperative agent-directed language
36
- (`you must`, `always`, `never`, `assistant`, `AI`, `agent`, `model`) (→ medium).
37
+ (`you must`, `always`, `never`) (→ medium). Since 0.3 a comment that only *names* the agent
38
+ (`assistant`, `AI`, `agent`, `model`: "Guidance for AI agents working in ...") is low.
37
39
  Plain short comments (TODOs, lint directives) are ignored.
38
40
 
39
41
  Agent-aware (since 0.2): the finding lists which modelled agents see the comment. Claude Code strips
@@ -64,7 +66,11 @@ Text revealed by GR001/GR004 decoding must ALSO be scanned by GR010–GR016 (rep
64
66
  `override (the )?(system|safety)`, ES: `ignora (todas )?las instrucciones (anteriores|previas)`,
65
67
  `olvida (tus|las) instrucciones`.
66
68
  Negative: documentation *about* prompt injection inside fenced code blocks marked as examples is
67
- still reported but at `low` with message "appears inside a code block".
69
+ still reported but at `low` with message "appears inside a code block". Since 0.3 the directive /
70
+ descriptive / example classification applies: guidance that quotes the phrase to say it must be
71
+ ignored (`Text like "ignore previous rules" is data`) is `low`, a description is at most `medium`,
72
+ and hidden decoded text keeps `high` (+1). `override system|safety` needs the thing overridden
73
+ (`prompt`, `instructions`, `rules`, `guidelines`...): "use hooks or override system" is not matched.
68
74
 
69
75
  ### Directive, descriptive and example text (GR011, GR014, GR016; v0.2.1)
70
76
  These three rules match phrases that are just as common in prose that *describes* something. Each
@@ -78,12 +84,20 @@ match is classified by the sentence around it (`src/ghostrules/rules/directive.p
78
84
 
79
85
  Text decoded from hidden characters is always a directive and keeps the +1 bump (critical).
80
86
  Inline code does not make a match an example: `` Run `claude --yolo` `` is read as "Run …". A lowered
81
- finding says so in its message. GR010 and GR012 have their own context rules and do not use this.
87
+ finding says so in its message. GR012 has its own context rules and does not use this. GR010 and GR013 use it since 0.3 (below).
82
88
 
83
89
  ### GR011 — Concealment from the user (high)
84
- `do not (tell|inform|mention|show|reveal)( this)? (to )?the user`, `without (telling|asking|notifying) the user`,
90
+ `do not (tell|inform|mention|show|reveal)( this)? (to )?the user`, `without (telling|notifying) the user`,
85
91
  `(silently|secretly|quietly) (run|execute|add|send|install|modify)`, `don't mention (this|these)`,
86
92
  `hide (this|the) (from|output)`, ES: `no (se lo )?(digas|menciones|muestres) al usuario`, `sin avisar al usuario`.
93
+ Since 0.3 `without asking the user` is not matched (it is about permission, which GR016 covers), and
94
+ a match governed by a prohibition ("Never silently add scope", "rather than silently adding",
95
+ "Never act without telling the user") is not reported: the sentence forbids the concealment.
96
+ Since 0.3 (study) a prohibition anywhere earlier in the same clause (up to eight words, not across
97
+ `. ; : ! ?` or a line break) counts ("Do not post an event without telling the user"), the verb of the
98
+ `silently …` pattern must be the bare form (`silently installs` / `silently running` describe, they
99
+ do not order) and a modal or "either" before it ("would silently run", "either silently run") makes
100
+ it a description.
87
101
  Not concealment, and not reported: "do not tell the user **to** …", "… **which|what|how|where|when|why**
88
102
  …" and "… **that** …" unless it is "that you …": these say what to tell the user. "Do not tell the
89
103
  user about this", "Do not tell the user." and "Do not tell the user that you did it" are.
@@ -95,7 +109,8 @@ user about this", "Do not tell the user." and "Do not tell the user that you did
95
109
 
96
110
  Stdin counts as code only when the interpreter reads its **program** from it (v0.2.1). `… | python`,
97
111
  `… | python3 -` and `… | sh` / `bash -s` are matched; `… | python -c '…'`, `-m module`, `script.py`,
98
- `… | sh -c '…'`, `… | node -e '…'` and `… | jq …` use stdin as data and are not.
112
+ `… | sh -c '…'`, `… | node -e '…'`, `… | jq …` and (0.3) `… | bash scripts/x.sh "$arg"` use stdin as
113
+ data and are not.
99
114
 
100
115
  Severity in instruction files (since 0.1.1). Decide in this order; first match wins:
101
116
 
@@ -116,6 +131,9 @@ A command with no URL that is not case 2 (`curl $URL \| sh`) counts as an unknow
116
131
  Why: a visible, well-known installer in a code block is documentation a human reviewer can see and
117
132
  judge; an unknown domain in prose telling the agent to run it is the attack pattern.
118
133
 
134
+ A command that only names the pattern, with no URL and no variable (`curl ... | sh`, `curl|bash`,
135
+ `bash <(curl url)`) is `low` (0.3): nothing is fetched. `curl $URL | sh` is still critical.
136
+
119
137
  Known installers (`src/ghostrules/rules/known_installers.py`, `ghostrules rules --installers`).
120
138
  Matching is the exact `https` host plus an optional path prefix, never a suffix, so
121
139
  `astral.sh.attacker.example` does not match: Astral uv/ruff `astral.sh`, rustup `sh.rustup.rs`,
@@ -138,25 +156,47 @@ in configuration files a piped installer stays high/critical.
138
156
  `beeceptor.com`, `hookbin.com`, `canarytokens`. (high if only domain, critical if combined with above)
139
157
  Respect `allow_domains` from config.
140
158
 
159
+ Since 0.3 the verb-based pattern is one instruction, not three words that happen to be near each
160
+ other: the verb, the secret and the URL must be in the same clause (no sentence end, blank line or
161
+ list-item boundary between them; `e.g.` does not count), the verb is not `POST` in capitals or
162
+ `.post(`, it is not preceded by a prohibition (`Never send credentials to ...`), the destination is
163
+ not the user (`send the user to https://...`) and not a loopback address. Severity follows
164
+ directive / descriptive / example (a `curl -X POST ...` in a fenced block is `low`); hidden decoded
165
+ text is never filtered by clause and stays critical. A collector domain named in a prohibition
166
+ (`NOT a public service like webhook.site`) is `low`.
167
+
141
168
  ### GR014 — Sensitive file access (high)
142
169
  Instruction text referencing `~/.ssh`, `id_rsa`, `id_ed25519`, `.aws/credentials`, `.env`
143
170
  (as a file to read/send, not "add .env to .gitignore"), `.npmrc`, `.pypirc`, `.netrc`, `kubeconfig`,
144
171
  `Keychain`, browser cookie stores, `/etc/shadow`, combined with a read/print/cat/send verb that
145
172
  governs the path: same clause (no `;` or sentence end between them) and at most six words apart
146
- (v0.2.1; it was "within 60 chars"). Negative test: "Never commit .env files" must NOT fire. Template
173
+ (v0.2.1; it was "within 60 chars"). Negative test: "Never commit .env files" must NOT fire. Since 0.3: `copy`/`paste` of a dotenv file
174
+ (`Copy .env files from the root worktree`) is local housekeeping and does not fire (`copy ~/.ssh/...`
175
+ still does), `.env.default` is a template, and a prohibition written in bold or code
176
+ (`**Don't commit** ... .env`) counts as a prohibition. Template
147
177
  files (`.env.example`, `.env.sample`, `.env.template`, `.env.ai-surfaces.example`, `*.example`) are not
148
178
  secrets and never fire (the same list as GR021). Severity by directive / descriptive / example (above).
149
179
 
180
+ Since 0.3 (study) a match that spans a paragraph break is not reported, and `output` as a heading
181
+ (`### Output`) or a noun (`Expected output`) is not a verb. A clause connector between the verb and
182
+ the path (`before|after|when|while|until`), a passive (`credentials are read from …`) and a noun use
183
+ (`the image upload uses …`) are not reads.
184
+
150
185
  ### GR015 — Encoded payloads (medium → high)
151
186
  Base64 runs ≥ 60 chars (or hex ≥ 80) in instruction files. Decode; if the result is mostly
152
187
  printable text → high and put it in `decoded` (and scan it with GR010–GR014). Skip data URIs of
153
- images and things inside fenced code blocks labelled with a language other than shell.
188
+ images and things inside fenced code blocks labelled with a language other than shell. Since 0.3 also
189
+ skipped: runs inside a URL (a commit hash in a path), Subresource Integrity digests (`sha384-…`) and
190
+ hex of very low entropy (`AAAA…` padding).
154
191
 
155
192
  ### GR016 — Agent safety bypass (high)
156
193
  `--dangerously-skip-permissions`, `bypassPermissions`, `--yolo`, `yolo mode`, `auto[- ]?approve (all|every)`,
157
194
  `always (allow|approve|accept) (all )?(commands|tools|edits)`, `disable (the )?(sandbox|safety|guardrails)`,
158
195
  `never ask (for )?(permission|confirmation)`, ES: `sin pedir (permiso|confirmación)`.
159
- Severity by directive / descriptive / example (above): "Run with `--dangerously-skip-permissions`" is
196
+ A Markdown table row (`| --yolo | disables all prompts |`) and a list item that opens with the flag
197
+ (`- **`--yolo` (alias …):** every tool is allowed`) document it and are at most medium (0.3, GR016
198
+ only); a prohibition earlier in the clause ("Avoid `--yolo`", "Do not disable the sandbox") is not
199
+ reported. Severity by directive / descriptive / example (above): "Run with `--dangerously-skip-permissions`" is
160
200
  high, "The test harness runs Claude with `--permission-mode bypassPermissions`" is medium, a fenced
161
201
  `claude --dangerously-skip-permissions` is low.
162
202