ghostrules 0.1.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 (119) hide show
  1. ghostrules-0.1.0/.ghostrules.toml +10 -0
  2. ghostrules-0.1.0/.gitattributes +5 -0
  3. ghostrules-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. ghostrules-0.1.0/.github/ISSUE_TEMPLATE/false-positive.yml +49 -0
  5. ghostrules-0.1.0/.github/ISSUE_TEMPLATE/new-rule.yml +52 -0
  6. ghostrules-0.1.0/.github/workflows/ci.yml +40 -0
  7. ghostrules-0.1.0/.github/workflows/release.yml +77 -0
  8. ghostrules-0.1.0/.github/workflows/self-test.yml +73 -0
  9. ghostrules-0.1.0/.gitignore +14 -0
  10. ghostrules-0.1.0/.pre-commit-hooks.yaml +9 -0
  11. ghostrules-0.1.0/CHANGELOG.md +41 -0
  12. ghostrules-0.1.0/CLAUDE.md +60 -0
  13. ghostrules-0.1.0/CONTRIBUTING.md +80 -0
  14. ghostrules-0.1.0/LICENSE +21 -0
  15. ghostrules-0.1.0/PKG-INFO +333 -0
  16. ghostrules-0.1.0/README.md +278 -0
  17. ghostrules-0.1.0/SECURITY.md +48 -0
  18. ghostrules-0.1.0/action.yml +97 -0
  19. ghostrules-0.1.0/demo/demo.tape +39 -0
  20. ghostrules-0.1.0/docs/DECISIONS.md +320 -0
  21. ghostrules-0.1.0/docs/PLAN.md +101 -0
  22. ghostrules-0.1.0/docs/RULES.md +147 -0
  23. ghostrules-0.1.0/docs/SPEC.md +158 -0
  24. ghostrules-0.1.0/examples/clean-repo/.claude/settings.json +12 -0
  25. ghostrules-0.1.0/examples/clean-repo/.mcp.json +25 -0
  26. ghostrules-0.1.0/examples/clean-repo/AGENTS.md +11 -0
  27. ghostrules-0.1.0/examples/clean-repo/CLAUDE.md +5 -0
  28. ghostrules-0.1.0/examples/clean-repo/README.md +7 -0
  29. ghostrules-0.1.0/examples/poisoned-repo/.claude/settings.json +9 -0
  30. ghostrules-0.1.0/examples/poisoned-repo/.claude/skills/setup/SKILL.md +9 -0
  31. ghostrules-0.1.0/examples/poisoned-repo/.cursorrules +4 -0
  32. ghostrules-0.1.0/examples/poisoned-repo/.github/copilot-instructions.md +7 -0
  33. ghostrules-0.1.0/examples/poisoned-repo/.mcp.json +42 -0
  34. ghostrules-0.1.0/examples/poisoned-repo/AGENTS.md +6 -0
  35. ghostrules-0.1.0/examples/poisoned-repo/CLAUDE.md +5 -0
  36. ghostrules-0.1.0/examples/poisoned-repo/README.md +24 -0
  37. ghostrules-0.1.0/pyproject.toml +71 -0
  38. ghostrules-0.1.0/release-notes.md +32 -0
  39. ghostrules-0.1.0/scripts/changelog_section.py +48 -0
  40. ghostrules-0.1.0/scripts/gen_rules_table.py +65 -0
  41. ghostrules-0.1.0/scripts/make_demo.py +278 -0
  42. ghostrules-0.1.0/scripts/make_fixtures.py +204 -0
  43. ghostrules-0.1.0/scripts/set_owner.py +72 -0
  44. ghostrules-0.1.0/src/ghostrules/__init__.py +6 -0
  45. ghostrules-0.1.0/src/ghostrules/_compat.py +12 -0
  46. ghostrules-0.1.0/src/ghostrules/clean.py +41 -0
  47. ghostrules-0.1.0/src/ghostrules/cli.py +319 -0
  48. ghostrules-0.1.0/src/ghostrules/config.py +93 -0
  49. ghostrules-0.1.0/src/ghostrules/jsonloc.py +194 -0
  50. ghostrules-0.1.0/src/ghostrules/markdown_utils.py +59 -0
  51. ghostrules-0.1.0/src/ghostrules/mcp_config.py +243 -0
  52. ghostrules-0.1.0/src/ghostrules/models.py +188 -0
  53. ghostrules-0.1.0/src/ghostrules/py.typed +0 -0
  54. ghostrules-0.1.0/src/ghostrules/reporters/__init__.py +1 -0
  55. ghostrules-0.1.0/src/ghostrules/reporters/common.py +86 -0
  56. ghostrules-0.1.0/src/ghostrules/reporters/json_.py +45 -0
  57. ghostrules-0.1.0/src/ghostrules/reporters/markdown.py +55 -0
  58. ghostrules-0.1.0/src/ghostrules/reporters/sarif.py +137 -0
  59. ghostrules-0.1.0/src/ghostrules/reporters/table.py +235 -0
  60. ghostrules-0.1.0/src/ghostrules/reveal.py +90 -0
  61. ghostrules-0.1.0/src/ghostrules/rules/__init__.py +16 -0
  62. ghostrules-0.1.0/src/ghostrules/rules/agent_settings.py +159 -0
  63. ghostrules-0.1.0/src/ghostrules/rules/helpers.py +118 -0
  64. ghostrules-0.1.0/src/ghostrules/rules/injection.py +317 -0
  65. ghostrules-0.1.0/src/ghostrules/rules/mcp.py +445 -0
  66. ghostrules-0.1.0/src/ghostrules/rules/patterns.py +187 -0
  67. ghostrules-0.1.0/src/ghostrules/rules/registry.py +78 -0
  68. ghostrules-0.1.0/src/ghostrules/rules/unicode.py +503 -0
  69. ghostrules-0.1.0/src/ghostrules/scanner.py +244 -0
  70. ghostrules-0.1.0/src/ghostrules/secrets.py +219 -0
  71. ghostrules-0.1.0/src/ghostrules/targets.py +220 -0
  72. ghostrules-0.1.0/src/ghostrules/unicode_utils.py +138 -0
  73. ghostrules-0.1.0/tests/conftest.py +43 -0
  74. ghostrules-0.1.0/tests/data/sarif-schema-2.1.0.json +3389 -0
  75. ghostrules-0.1.0/tests/fixtures/unicode/gr001_flag_forged/AGENTS.md +1 -0
  76. ghostrules-0.1.0/tests/fixtures/unicode/gr001_flag_ok/AGENTS.md +1 -0
  77. ghostrules-0.1.0/tests/fixtures/unicode/gr001_tags/AGENTS.md +4 -0
  78. ghostrules-0.1.0/tests/fixtures/unicode/gr002_bidi/AGENTS.md +2 -0
  79. ghostrules-0.1.0/tests/fixtures/unicode/gr002_rtl_medium/AGENTS.md +3 -0
  80. ghostrules-0.1.0/tests/fixtures/unicode/gr003_bom_midfile/AGENTS.md +2 -0
  81. ghostrules-0.1.0/tests/fixtures/unicode/gr003_bom_ok/AGENTS.md +1 -0
  82. ghostrules-0.1.0/tests/fixtures/unicode/gr003_emoji_zwj_ok/AGENTS.md +2 -0
  83. ghostrules-0.1.0/tests/fixtures/unicode/gr003_indic_ok/AGENTS.md +1 -0
  84. ghostrules-0.1.0/tests/fixtures/unicode/gr003_persian_ok/AGENTS.md +1 -0
  85. ghostrules-0.1.0/tests/fixtures/unicode/gr003_soft_hyphen/AGENTS.md +1 -0
  86. ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_file_high/AGENTS.md +20 -0
  87. ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_line_high/AGENTS.md +1 -0
  88. ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_medium/AGENTS.md +1 -0
  89. ghostrules-0.1.0/tests/fixtures/unicode/gr004_vs_bytes/AGENTS.md +1 -0
  90. ghostrules-0.1.0/tests/fixtures/unicode/gr004_vs_ok/AGENTS.md +2 -0
  91. ghostrules-0.1.0/tests/fixtures/unicode/gr005_benign_ok/CLAUDE.md +2 -0
  92. ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_imperative/CLAUDE.md +2 -0
  93. ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_injection/CLAUDE.md +3 -0
  94. ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_multiline/CLAUDE.md +5 -0
  95. ghostrules-0.1.0/tests/fixtures/unicode/gr005_in_code_block_ok/CLAUDE.md +4 -0
  96. ghostrules-0.1.0/tests/fixtures/unicode/gr005_short_ok/CLAUDE.md +2 -0
  97. ghostrules-0.1.0/tests/fixtures/unicode/gr005_unterminated/CLAUDE.md +4 -0
  98. ghostrules-0.1.0/tests/fixtures/unicode/gr006_ok/AGENTS.md +3 -0
  99. ghostrules-0.1.0/tests/fixtures/unicode/gr006_url/AGENTS.md +1 -0
  100. ghostrules-0.1.0/tests/fixtures/unicode/gr006_word/AGENTS.md +1 -0
  101. ghostrules-0.1.0/tests/secret_samples.py +36 -0
  102. ghostrules-0.1.0/tests/test_ci_files.py +372 -0
  103. ghostrules-0.1.0/tests/test_cli.py +409 -0
  104. ghostrules-0.1.0/tests/test_config.py +75 -0
  105. ghostrules-0.1.0/tests/test_demo.py +187 -0
  106. ghostrules-0.1.0/tests/test_docs.py +203 -0
  107. ghostrules-0.1.0/tests/test_markdown_utils.py +67 -0
  108. ghostrules-0.1.0/tests/test_mcp_config.py +154 -0
  109. ghostrules-0.1.0/tests/test_models.py +39 -0
  110. ghostrules-0.1.0/tests/test_patterns.py +96 -0
  111. ghostrules-0.1.0/tests/test_reporters.py +345 -0
  112. ghostrules-0.1.0/tests/test_rules_config.py +491 -0
  113. ghostrules-0.1.0/tests/test_rules_injection.py +415 -0
  114. ghostrules-0.1.0/tests/test_rules_unicode.py +319 -0
  115. ghostrules-0.1.0/tests/test_scanner.py +363 -0
  116. ghostrules-0.1.0/tests/test_secret_masking.py +256 -0
  117. ghostrules-0.1.0/tests/test_secrets.py +138 -0
  118. ghostrules-0.1.0/tests/test_targets.py +183 -0
  119. ghostrules-0.1.0/tests/test_unicode_utils.py +75 -0
@@ -0,0 +1,10 @@
1
+ # ghostrules scanning its own repository: the demos and fixtures are poisoned on purpose.
2
+ exclude = [
3
+ "examples/**",
4
+ "tests/fixtures/**",
5
+ "dist/**",
6
+ "build/**",
7
+ ".mypy_cache/**",
8
+ ".ruff_cache/**",
9
+ ".pytest_cache/**",
10
+ ]
@@ -0,0 +1,5 @@
1
+ # Keep line endings stable: the demo repositories and fixtures are byte-exact (they contain
2
+ # invisible characters and tests compare them with what the generators write).
3
+ * text=auto eol=lf
4
+ examples/** -text
5
+ tests/fixtures/** -text
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Report a security vulnerability
4
+ url: https://github.com/BardLaudian/ghostrules/security/advisories/new
5
+ about: Please report vulnerabilities in ghostrules itself privately, not in a public issue.
@@ -0,0 +1,49 @@
1
+ name: False positive
2
+ description: ghostrules reported something that is not a problem.
3
+ title: "[false positive] GRxxx: "
4
+ labels: ["false-positive"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for reporting it. **Do not paste real secrets or private files.** Reduce the content to
10
+ a small, defanged example (use `example.com`, fake tokens). If the finding involves invisible
11
+ characters, say which ones (for example the output of `ghostrules reveal FILE`).
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: ghostrules version
16
+ description: Output of `ghostrules version`.
17
+ validations:
18
+ required: true
19
+ - type: input
20
+ id: rule
21
+ attributes:
22
+ label: Rule
23
+ description: The rule that fired, for example GR013.
24
+ placeholder: GR013
25
+ validations:
26
+ required: true
27
+ - type: textarea
28
+ id: input
29
+ attributes:
30
+ label: Minimal file that triggers it
31
+ description: The smallest defanged snippet that reproduces the finding, and its file name.
32
+ render: text
33
+ validations:
34
+ required: true
35
+ - type: textarea
36
+ id: output
37
+ attributes:
38
+ label: What ghostrules printed
39
+ description: The finding (`ghostrules scan --no-color FILE`).
40
+ render: text
41
+ validations:
42
+ required: true
43
+ - type: textarea
44
+ id: why
45
+ attributes:
46
+ label: Why it is legitimate
47
+ description: What the text is for, and how a rule could tell it apart from a real problem.
48
+ validations:
49
+ required: true
@@ -0,0 +1,52 @@
1
+ name: New rule proposal
2
+ description: Suggest a new detection, or a missed one (false negative).
3
+ title: "[rule] "
4
+ labels: ["new-rule"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Good rules are precise: they say what to look for, why it is dangerous in a file an agent
10
+ obeys, and what must **not** match. **Use defanged examples only** (`example.com`, fake
11
+ tokens), never working payloads.
12
+ - type: textarea
13
+ id: threat
14
+ attributes:
15
+ label: The risk
16
+ description: What can an attacker do with it, and which agent or file does it affect? Link a primary source if there is one.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: positive
21
+ attributes:
22
+ label: Should be reported
23
+ description: One or more defanged examples that the rule must flag, with the file name.
24
+ render: text
25
+ validations:
26
+ required: true
27
+ - type: textarea
28
+ id: negative
29
+ attributes:
30
+ label: Must not be reported
31
+ description: Legitimate examples that look similar and must stay quiet.
32
+ render: text
33
+ validations:
34
+ required: true
35
+ - type: dropdown
36
+ id: severity
37
+ attributes:
38
+ label: Suggested severity
39
+ options: [critical, high, medium, low, info]
40
+ validations:
41
+ required: true
42
+ - type: textarea
43
+ id: detection
44
+ attributes:
45
+ label: Detection idea
46
+ description: A pattern, a heuristic or a config check. Optional but helpful.
47
+ - type: checkboxes
48
+ id: contribute
49
+ attributes:
50
+ label: Contribution
51
+ options:
52
+ - label: I can send a pull request (see CONTRIBUTING.md).
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest, macos-latest]
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Lint
24
+ run: ruff check .
25
+ - name: Format check
26
+ run: ruff format --check .
27
+ - name: Test
28
+ run: pytest -q
29
+ - name: Smoke test the demos
30
+ shell: bash
31
+ run: |
32
+ ghostrules scan examples/clean-repo
33
+ code=0
34
+ ghostrules scan examples/poisoned-repo --no-color > /dev/null || code=$?
35
+ test "$code" = "1"
36
+ - name: ghostrules scans its own repository
37
+ shell: bash
38
+ run: |
39
+ ghostrules scan . --no-color
40
+ ghostrules scan . --all-files --no-color
@@ -0,0 +1,77 @@
1
+ name: Release
2
+
3
+ # Push a tag like v0.1.0 to publish. PyPI uses Trusted Publishing (OIDC): there is no API token
4
+ # in this repository. One-time setup on PyPI: add a "pending publisher" for this repository,
5
+ # workflow "release.yml" and environment "pypi".
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: The tag must match the package version
24
+ env:
25
+ TAG: ${{ github.ref_name }}
26
+ run: |
27
+ python - <<'PY'
28
+ import os, re
29
+ version = re.search(r'__version__ = "([^"]+)"', open("src/ghostrules/__init__.py").read()).group(1)
30
+ tag = os.environ["TAG"].removeprefix("v")
31
+ assert version == tag, f"tag {tag} does not match __version__ {version}"
32
+ PY
33
+
34
+ - name: The changelog must have release notes for this tag
35
+ env:
36
+ TAG: ${{ github.ref_name }}
37
+ run: python scripts/changelog_section.py "$TAG" > release-notes.md
38
+
39
+ - name: Build
40
+ run: |
41
+ python -m pip install --disable-pip-version-check --quiet build
42
+ python -m build
43
+
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: |
48
+ dist/
49
+ release-notes.md
50
+
51
+ publish-pypi:
52
+ needs: build
53
+ runs-on: ubuntu-latest
54
+ environment: pypi
55
+ permissions:
56
+ id-token: write # Trusted Publishing
57
+ steps:
58
+ - uses: actions/download-artifact@v4
59
+ with:
60
+ name: dist
61
+ - uses: pypa/gh-action-pypi-publish@release/v1
62
+
63
+ github-release:
64
+ needs: publish-pypi
65
+ runs-on: ubuntu-latest
66
+ permissions:
67
+ contents: write
68
+ steps:
69
+ - uses: actions/download-artifact@v4
70
+ with:
71
+ name: dist
72
+ - name: Create the GitHub release
73
+ env:
74
+ GH_TOKEN: ${{ github.token }}
75
+ TAG: ${{ github.ref_name }}
76
+ REPO: ${{ github.repository }}
77
+ run: gh release create "$TAG" dist/* --repo "$REPO" --title "$TAG" --notes-file release-notes.md
@@ -0,0 +1,73 @@
1
+ name: Self-test
2
+
3
+ # Runs the action from this very checkout against the demo repositories.
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ action:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Poisoned demo with fail-on none (must pass, must report findings)
19
+ id: poisoned
20
+ uses: ./
21
+ with:
22
+ path: examples/poisoned-repo
23
+ fail-on: none
24
+ upload-sarif: "false"
25
+
26
+ - name: SARIF must contain results, led by the hidden instruction
27
+ run: |
28
+ python - <<'PY'
29
+ import json
30
+ run = json.load(open("ghostrules.sarif"))["runs"][0]
31
+ results = run["results"]
32
+ assert len(results) > 10, f"expected many results, got {len(results)}"
33
+ ids = {r["ruleId"] for r in results}
34
+ assert {"GR001", "GR012", "GR101"} <= ids, ids
35
+ assert results[0]["ruleId"] == "GR001", results[0]["ruleId"]
36
+ assert run["tool"]["driver"]["rules"], "rules missing"
37
+ print(f"ok: {len(results)} results")
38
+ PY
39
+
40
+ - name: The exit-code output is 0
41
+ env:
42
+ EXIT_CODE: ${{ steps.poisoned.outputs.exit-code }}
43
+ run: test "$EXIT_CODE" = "0"
44
+
45
+ - name: Poisoned demo with the default fail-on (must fail the step)
46
+ id: gate
47
+ uses: ./
48
+ continue-on-error: true
49
+ with:
50
+ path: examples/poisoned-repo
51
+ upload-sarif: "false"
52
+
53
+ - name: The gate must have failed
54
+ env:
55
+ OUTCOME: ${{ steps.gate.outcome }}
56
+ EXIT_CODE: ${{ steps.gate.outputs.exit-code }}
57
+ run: |
58
+ test "$OUTCOME" = "failure"
59
+ test "$EXIT_CODE" = "1"
60
+
61
+ - name: Clean demo (must pass with no results)
62
+ uses: ./
63
+ with:
64
+ path: examples/clean-repo
65
+ upload-sarif: "false"
66
+
67
+ - name: SARIF must be empty
68
+ run: |
69
+ python - <<'PY'
70
+ import json
71
+ results = json.load(open("ghostrules.sarif"))["runs"][0]["results"]
72
+ assert results == [], results
73
+ PY
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.ghostrules.bak
14
+ ghostrules.sarif
@@ -0,0 +1,9 @@
1
+ - id: ghostrules
2
+ name: ghostrules (hidden instructions in AI agent files)
3
+ description: >-
4
+ Scan the repository for hidden Unicode, malicious instructions and risky MCP/agent
5
+ configuration in AGENTS.md, CLAUDE.md, .cursorrules, skills, MCP configs and similar files.
6
+ entry: ghostrules scan
7
+ language: python
8
+ pass_filenames: false
9
+ minimum_pre_commit_version: "3.0.0"
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-10-27
9
+
10
+ First release.
11
+
12
+ ### Added
13
+ - Scanner for the files AI coding agents read and obey (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`,
14
+ Copilot instructions, skills, MCP configs, agent settings and more), with `--home` for
15
+ user-level configs and `--all-files` for hidden Unicode anywhere in a tree.
16
+ - 21 rules. Hidden content: Unicode tag characters (GR001), bidirectional controls (GR002),
17
+ zero-width characters (GR003), variation-selector steganography (GR004), hidden HTML comments
18
+ (GR005), mixed-script homoglyphs (GR006). Malicious instructions, English and Spanish: instruction
19
+ override (GR010), concealment (GR011), remote code execution (GR012), data exfiltration (GR013),
20
+ sensitive file access (GR014), encoded payloads (GR015), safety bypass (GR016). Configuration:
21
+ hardcoded secrets (GR101), unpinned packages (GR102), insecure transport (GR103), broad
22
+ filesystem scope (GR104), shell wrappers (GR105), dangerous Docker options (GR106),
23
+ over-permissive agent settings (GR107), auto-trusted MCP servers (GR108).
24
+ - Hidden text decoded by GR001, GR004 and GR015 is re-scanned by GR010-GR016 and reported with
25
+ one more level of severity.
26
+ - CLI: `scan` (the default command), `reveal`, `clean`, `rules`, `version`. Reports as a table
27
+ grouped by file with the decoded hidden text highlighted, JSON, SARIF 2.1.0 (validated against the
28
+ official schema) and Markdown. Exit codes: 0 clean, 1 findings at or above `--fail-on`, 2 error.
29
+ - Configuration through `.ghostrules.toml` or `[tool.ghostrules]`, and inline `ghostrules-ignore`
30
+ suppressions (never for GR001, GR002 or text found inside hidden characters).
31
+ - GitHub Action (SARIF upload to code scanning, job summary, `fail-on` gate), pre-commit hook, CI
32
+ with a smoke test and a self-scan, and a tag-triggered release workflow using PyPI Trusted
33
+ Publishing.
34
+ - Demo repositories `examples/poisoned-repo` (every rule fires) and `examples/clean-repo` (no
35
+ findings), and `demo/demo.tape` to record the README GIF.
36
+
37
+ ### Security
38
+ - Read-only: nothing found in a scanned file is executed, imported, downloaded or sent anywhere,
39
+ and the tool makes no network requests. Symbolic links are not followed.
40
+ - Secrets are never printed: every finding, warning and error passes through one masking step
41
+ (`ghp_****…a1b2`), and a test fails if a secret appears in any report format.
@@ -0,0 +1,60 @@
1
+ # ghostrules — project memory for Claude Code
2
+
3
+ ## What this project is
4
+ `ghostrules` is a Python CLI + GitHub Action that scans a repository (and optionally the user's
5
+ machine) for **hidden instructions and risky configuration in the files AI coding agents read and
6
+ obey**: `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, Copilot instructions, skills, MCP configs, etc.
7
+
8
+ Tagline: *"Find the instructions your AI agent sees but you don't."*
9
+
10
+ It is a **defensive** security tool. It only reads files and reports findings. It never executes
11
+ anything it finds, never makes network requests, and never phones home.
12
+
13
+ ## Source-of-truth documents (read before coding)
14
+ - `docs/SPEC.md` — features, CLI, file targets, output formats, config. **The contract.**
15
+ - `docs/RULES.md` — every detection rule: ID, severity, logic, true/false positive examples.
16
+ - `docs/PLAN.md` — build order, phase by phase, with acceptance criteria.
17
+
18
+ If something in the code disagrees with these docs, the docs win unless the user says otherwise.
19
+ If a doc is ambiguous, pick the simplest reasonable option and note it in `docs/DECISIONS.md`.
20
+
21
+ ## Tech stack
22
+ - Python **3.10+**, packaged with `pyproject.toml` (hatchling), src layout: `src/ghostrules/`.
23
+ - CLI: **typer**; terminal output: **rich**. TOML config: `tomllib` (3.11+) / `tomli` fallback.
24
+ - No other runtime dependencies unless clearly justified. **No network libraries at runtime.**
25
+ - Dev: `pytest`, `pytest-cov`, `ruff` (lint + format), `mypy` (lenient is fine).
26
+
27
+ ## Commands
28
+ ```bash
29
+ pip install -e ".[dev]" # install for development
30
+ pytest -q # run tests
31
+ ruff check . && ruff format . # lint + format
32
+ ghostrules scan examples/poisoned-repo # manual smoke test (must find issues)
33
+ ghostrules scan examples/clean-repo # must report zero findings
34
+ ```
35
+
36
+ ## Code conventions
37
+ - Each rule is a small class/function in `src/ghostrules/rules/` registered in one registry.
38
+ Rules receive a parsed `ScanTarget` (path, kind, text, lines, parsed JSON/TOML if any) and
39
+ yield `Finding` objects. Rules must be pure: no I/O, no global state.
40
+ - `Finding` fields: `rule_id, severity, title, message, path, line, column, end_column, snippet,
41
+ decoded (optional str), fix_hint`.
42
+ - Severities: `critical > high > medium > low > info`.
43
+ - Snippets must render invisible characters visibly, e.g. `⟦U+200B ZERO WIDTH SPACE⟧`.
44
+ - **Secrets must always be masked** in every output format (`ghp_****…a1b2`).
45
+ - Type hints everywhere; small functions; docstrings on public functions.
46
+ - Every rule needs at least one positive and one negative test.
47
+
48
+ ## Safety rules for this repo (important)
49
+ - Test fixtures and demo files must be **defanged**: use `attacker.example`, `example.com`,
50
+ `exfil.example`, fake tokens like `ghp_EXAMPLEEXAMPLEEXAMPLEEXAMPLE0000` — never real
51
+ domains, real webhooks or working payloads.
52
+ - Fixtures containing invisible characters are **generated by `scripts/make_fixtures.py`**,
53
+ not hand-typed, so they are reproducible and reviewable.
54
+ - Never add code that executes, downloads or evaluates scanned content.
55
+
56
+ ## Workflow
57
+ - Work phase by phase following `docs/PLAN.md`. After each phase: run tests + ruff, then
58
+ stop and summarize what was done and what is next.
59
+ - Commit after each phase with a conventional commit message (`feat:`, `test:`, `docs:`…).
60
+ - Keep the README in English and keep it updated when CLI flags change.
@@ -0,0 +1,80 @@
1
+ # Contributing to ghostrules
2
+
3
+ Thanks for helping. A noisy scanner gets uninstalled, so **precision matters as much as
4
+ coverage**: every change to a detection comes with a positive and a negative test.
5
+
6
+ ## Set up
7
+
8
+ ```bash
9
+ git clone https://github.com/BardLaudian/ghostrules && cd ghostrules
10
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
11
+ pip install -e ".[dev]"
12
+
13
+ pytest -q # tests
14
+ ruff check . && ruff format . # lint and format
15
+ mypy src # types (lenient is fine)
16
+ ghostrules scan examples/poisoned-repo # must find issues, exit code 1
17
+ ghostrules scan examples/clean-repo # must report nothing, exit code 0
18
+ ```
19
+
20
+ The documents in `docs/` are the contract: `SPEC.md` (features), `RULES.md` (every rule) and
21
+ `DECISIONS.md` (choices made where they were ambiguous). If code and docs disagree, fix one of
22
+ them in the same pull request.
23
+
24
+ ## Add a rule in three steps
25
+
26
+ 1. **Write the rule.** Add a function to the module that fits (`src/ghostrules/rules/unicode.py`,
27
+ `injection.py`, `mcp.py` or `agent_settings.py`) and register it with the decorator:
28
+
29
+ ```python
30
+ @rule(
31
+ "GR017", # next free id: GR0xx = instruction files, GR1xx = config
32
+ Severity.HIGH, # typical severity
33
+ "Short title",
34
+ "One sentence on what it finds and why it matters.",
35
+ ("instructions",), # file kinds it applies to
36
+ scans_decoded=True, # also run on hidden text decoded by other rules?
37
+ )
38
+ def gr017_something(target: ScanTarget) -> Iterator[Finding]:
39
+ ...
40
+ yield build_finding(target=target, rule_id="GR017", ...)
41
+ ```
42
+
43
+ Rules are **pure**: they receive a `ScanTarget` and yield `Finding`s, with no I/O and no global
44
+ state. Use `build_finding` so line, column, snippet and secret masking come out right. Put
45
+ shared regular expressions in `rules/patterns.py`.
46
+
47
+ 2. **Test it.** Add at least one positive and one negative test (see `tests/test_rules_*.py`). Add
48
+ a case to `scripts/make_demo.py` so `examples/poisoned-repo` triggers the rule (a test fails
49
+ if any rule does not fire there) and make sure `examples/clean-repo` still reports nothing.
50
+ Fixtures with invisible characters are generated by `scripts/make_fixtures.py`.
51
+
52
+ 3. **Document it.** Describe the rule in `docs/RULES.md` (ID, severity, logic, true and false
53
+ positive examples), note any judgement call in `docs/DECISIONS.md`, add a line to
54
+ `CHANGELOG.md`, then refresh the README table:
55
+
56
+ ```bash
57
+ python scripts/make_demo.py
58
+ python scripts/gen_rules_table.py --write
59
+ ```
60
+
61
+ ## Rules of the repository
62
+
63
+ - **Defanged only.** Fixtures, demos and tests use `attacker.example`, `exfil.example`,
64
+ `example.com` and obviously fake tokens such as `ghp_` + `EXAMPLE` repeated. Never real
65
+ domains, real webhooks or working payloads.
66
+ - **Invisible characters are generated, never typed.** Write them as escapes or with `chr()` in
67
+ the generators. A test fails if a literal invisible character ends up in the sources.
68
+ - **Never execute, download or evaluate scanned content**, and no network libraries at runtime.
69
+ - **Secrets are always masked.** Anything that prints text from a scanned file goes through
70
+ `ghostrules.secrets.mask_secrets` (findings do it for you).
71
+ - Type hints everywhere, small functions, docstrings on public functions, English README.
72
+ - Conventional commits (`feat:`, `fix:`, `test:`, `docs:`...).
73
+
74
+ ## Pull request checklist
75
+
76
+ - [ ] `pytest -q`, `ruff check .`, `ruff format --check .` and `mypy src` pass
77
+ - [ ] positive and negative tests for every new or changed detection
78
+ - [ ] `examples/poisoned-repo` still triggers every rule; `examples/clean-repo` is still clean
79
+ - [ ] `docs/RULES.md`, `docs/DECISIONS.md`, `CHANGELOG.md` and the README table are updated
80
+ - [ ] nothing real in the fixtures: no secrets, domains or payloads that work
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pablo Seoane Temprano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.