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.
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.ghostrules.lock +1 -1
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.ghostrules.toml +2 -0
- ghostrules-0.3.0/.github/workflows/corpus.yml +79 -0
- ghostrules-0.3.0/.github/workflows/pages.yml +48 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.gitignore +6 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/CHANGELOG.md +53 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/PKG-INFO +26 -3
- {ghostrules-0.2.1 → ghostrules-0.3.0}/README.md +20 -2
- {ghostrules-0.2.1 → ghostrules-0.3.0}/action.yml +2 -4
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/DECISIONS.md +147 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/RULES.md +49 -9
- {ghostrules-0.2.1 → ghostrules-0.3.0}/pyproject.toml +9 -0
- ghostrules-0.3.0/release-notes.md +50 -0
- ghostrules-0.3.0/scripts/build_site.py +41 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/__init__.py +1 -1
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/imports.py +3 -0
- ghostrules-0.3.0/src/ghostrules/playground.py +151 -0
- ghostrules-0.3.0/src/ghostrules/reporters/context.py +161 -0
- ghostrules-0.2.1/src/ghostrules/reporters/context.py → ghostrules-0.3.0/src/ghostrules/reporters/context_data.py +3 -147
- ghostrules-0.3.0/src/ghostrules/reveal.py +37 -0
- ghostrules-0.3.0/src/ghostrules/reveal_data.py +107 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/injection.py +200 -15
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/patterns.py +29 -9
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/unicode.py +23 -2
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/sensitive.py +3 -1
- ghostrules-0.3.0/study/analyze.py +278 -0
- ghostrules-0.3.0/study/charts/01-adoption-dark.svg +245 -0
- ghostrules-0.3.0/study/charts/01-adoption-light.svg +245 -0
- ghostrules-0.3.0/study/charts/02-tools-per-repo-dark.svg +143 -0
- ghostrules-0.3.0/study/charts/02-tools-per-repo-light.svg +143 -0
- ghostrules-0.3.0/study/charts/03-claude-agents-dark.svg +126 -0
- ghostrules-0.3.0/study/charts/03-claude-agents-light.svg +126 -0
- ghostrules-0.3.0/study/charts/04-what-agents-read-dark.svg +143 -0
- ghostrules-0.3.0/study/charts/04-what-agents-read-light.svg +143 -0
- ghostrules-0.3.0/study/charts/05-risk-dark.svg +126 -0
- ghostrules-0.3.0/study/charts/05-risk-light.svg +126 -0
- ghostrules-0.3.0/study/charts/06-precision-dark.svg +432 -0
- ghostrules-0.3.0/study/charts/06-precision-light.svg +432 -0
- ghostrules-0.3.0/study/charts.py +283 -0
- ghostrules-0.3.0/study/data/adoption_families.csv +13 -0
- ghostrules-0.3.0/study/data/claude_agents_relation.csv +5 -0
- ghostrules-0.3.0/study/data/conditional_rules.csv +5 -0
- ghostrules-0.3.0/study/data/findings_by_rule.csv +33 -0
- ghostrules-0.3.0/study/data/headline.json +18 -0
- ghostrules-0.3.0/study/data/imports.csv +6 -0
- ghostrules-0.3.0/study/data/instruction_file_lines.csv +6 -0
- ghostrules-0.3.0/study/data/numbers.csv +12 -0
- ghostrules-0.3.0/study/data/precision_by_rule.csv +15 -0
- ghostrules-0.3.0/study/data/precision_runs.csv +5 -0
- ghostrules-0.3.0/study/data/risk_indicators.csv +7 -0
- ghostrules-0.3.0/study/data/sample.csv +3 -0
- ghostrules-0.3.0/study/data/skills_concentration.csv +7 -0
- ghostrules-0.3.0/study/data/tools_per_repo.csv +8 -0
- ghostrules-0.3.0/study/features.py +138 -0
- ghostrules-0.3.0/study/label_classes.py +133 -0
- ghostrules-0.3.0/study/labels.toml +2646 -0
- ghostrules-0.3.0/study/report.md +166 -0
- ghostrules-0.3.0/study/report.py +211 -0
- ghostrules-0.3.0/tests/corpus_fakes.py +103 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_ci_files.py +22 -2
- ghostrules-0.3.0/tests/test_core_without_cli_deps.py +74 -0
- ghostrules-0.3.0/tests/test_corpus_packaging.py +31 -0
- ghostrules-0.3.0/tests/test_corpus_tools.py +634 -0
- ghostrules-0.3.0/tests/test_playground.py +62 -0
- ghostrules-0.3.0/tests/test_precision_study.py +164 -0
- ghostrules-0.3.0/tests/test_precision_v030.py +343 -0
- ghostrules-0.3.0/tests/test_study.py +138 -0
- ghostrules-0.3.0/tests/web/test_playground_smoke.py +99 -0
- ghostrules-0.3.0/web/app.js +376 -0
- ghostrules-0.3.0/web/index.html +77 -0
- ghostrules-0.3.0/web/style.css +101 -0
- ghostrules-0.2.1/release-notes.md +0 -43
- ghostrules-0.2.1/src/ghostrules/reveal.py +0 -90
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.gitattributes +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/false-positive.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/ISSUE_TEMPLATE/new-rule.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/ci.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/release.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.github/workflows/self-test.yml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/.pre-commit-hooks.yaml +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/CONTRIBUTING.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/LICENSE +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/SECURITY.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/README.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/demo.gif +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/demo.tape +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/imports.gif +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/imports.tape +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/demo/lock.tape +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/AGENT_MODELS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/PLAN.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/SPEC.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.1.1/GR012-FIX.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/PLAN.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/RULES.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/docs/design/v0.2/SPEC.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/.claude/settings.json +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/.mcp.json +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/clean-repo/README.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/.claude/rules/api.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/.env +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/README.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/docs/setup.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/docs/style.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/shared/team-rules.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/src/api/users.ts +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/import-chain-repo/src/legacy/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.claude/settings.json +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.claude/skills/setup/SKILL.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.cursorrules +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.github/copilot-instructions.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/.mcp.json +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/examples/poisoned-repo/README.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/changelog_section.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/gen_rules_table.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/make_demo.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/make_fixtures.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/scripts/set_owner.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/_compat.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/__init__.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/base.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/claude_code.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/copilot_cli.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/cursor.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/generic.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/agents/repo.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/clean.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/cli.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/config.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/context_command.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/contextscan.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/globs.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/jsonloc.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/lock.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/markdown_utils.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/mcp_config.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/models.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/py.typed +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/__init__.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/common.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/json_.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/lock.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/markdown.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/sarif.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/reporters/table.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/__init__.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/agent_settings.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/context_rules.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/directive.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/helpers.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/known_installers.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/mcp.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/rules/registry.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/scancontext.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/scanner.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/secrets.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/targets.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/src/ghostrules/unicode_utils.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/conftest.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/data/sarif-schema-2.1.0.json +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_flag_forged/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_flag_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr001_tags/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr002_bidi/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr002_rtl_medium/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_bom_midfile/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_bom_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_emoji_zwj_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_indic_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_persian_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_soft_hyphen/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_file_high/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_line_high/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr003_zw_medium/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr004_vs_bytes/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr004_vs_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_benign_ok/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_imperative/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_injection/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_comment_multiline/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_in_code_block_ok/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_short_ok/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr005_unterminated/CLAUDE.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_ok/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_url/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/fixtures/unicode/gr006_word/AGENTS.md +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/secret_samples.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_claude.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_others.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_agents_repo.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_cli.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_config.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_context_command.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_demo.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_directive.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_docs.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_globs.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_imports.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_lock.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_markdown_utils.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_mcp_config.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_models.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_patterns.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_precision_v021.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_reporters.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_config.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_injection.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_rules_unicode.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_scan_imports.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_scanner.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_secret_masking.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_secrets.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_symlink_alias.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_targets.py +0 -0
- {ghostrules-0.2.1 → ghostrules-0.3.0}/tests/test_unicode_utils.py +0 -0
|
@@ -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
|
|
@@ -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.
|
|
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
|
[](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml)
|
|
61
68
|
[](https://pypi.org/project/ghostrules/)
|
|
62
69
|
[](https://pypi.org/project/ghostrules/)
|
|
70
|
+
[](https://github.com/BardLaudian/ghostrules/blob/main/corpus/README.md)
|
|
63
71
|
[](https://github.com/BardLaudian/ghostrules/blob/main/LICENSE)
|
|
64
72
|
|
|
65
73
|

|
|
@@ -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.
|
|
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.
|
|
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
|
[](https://github.com/BardLaudian/ghostrules/actions/workflows/ci.yml)
|
|
6
8
|
[](https://pypi.org/project/ghostrules/)
|
|
7
9
|
[](https://pypi.org/project/ghostrules/)
|
|
10
|
+
[](https://github.com/BardLaudian/ghostrules/blob/main/corpus/README.md)
|
|
8
11
|
[](https://github.com/BardLaudian/ghostrules/blob/main/LICENSE)
|
|
9
12
|
|
|
10
13
|

|
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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|
|
|
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 '…'
|
|
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.
|
|
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
|
-
|
|
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
|
|