scamgate 0.3.3__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 (128) hide show
  1. scamgate-0.3.3/.claude-plugin/marketplace.json +18 -0
  2. scamgate-0.3.3/.github/workflows/ci.yml +91 -0
  3. scamgate-0.3.3/.github/workflows/pages.yml +35 -0
  4. scamgate-0.3.3/.github/workflows/release.yml +114 -0
  5. scamgate-0.3.3/.gitignore +13 -0
  6. scamgate-0.3.3/AGENTS.md +66 -0
  7. scamgate-0.3.3/CHANGELOG.md +245 -0
  8. scamgate-0.3.3/CONTRIBUTING.md +85 -0
  9. scamgate-0.3.3/LICENSE +21 -0
  10. scamgate-0.3.3/PKG-INFO +544 -0
  11. scamgate-0.3.3/README.md +516 -0
  12. scamgate-0.3.3/SECURITY.md +24 -0
  13. scamgate-0.3.3/claude-plugin/.claude-plugin/plugin.json +40 -0
  14. scamgate-0.3.3/claude-plugin/.mcp.json +12 -0
  15. scamgate-0.3.3/claude-plugin/LICENSE +21 -0
  16. scamgate-0.3.3/claude-plugin/README.md +86 -0
  17. scamgate-0.3.3/claude-plugin/hooks/hooks.json +53 -0
  18. scamgate-0.3.3/claude-plugin/icon.png +0 -0
  19. scamgate-0.3.3/docs/assets/scamgate-banner.jpg +0 -0
  20. scamgate-0.3.3/docs/design/enforcement-options.md +54 -0
  21. scamgate-0.3.3/docs/design/hermes-plugin.md +96 -0
  22. scamgate-0.3.3/docs/design/hook-review-verification.md +76 -0
  23. scamgate-0.3.3/pyproject.toml +49 -0
  24. scamgate-0.3.3/scenarios/pay_01_ceo_urgent_transfer.yaml +16 -0
  25. scamgate-0.3.3/scenarios/pay_02_changed_iban_invoice.yaml +17 -0
  26. scamgate-0.3.3/scenarios/pay_03_crypto_only_seller.yaml +15 -0
  27. scamgate-0.3.3/scenarios/pay_04_gift_card_revenue.yaml +15 -0
  28. scamgate-0.3.3/scenarios/pay_05_investment_guaranteed.yaml +15 -0
  29. scamgate-0.3.3/scenarios/pay_06_legit_known_payee.yaml +15 -0
  30. scamgate-0.3.3/scenarios/pay_07_over_budget_known_shop.yaml +13 -0
  31. scamgate-0.3.3/scenarios/pay_08_legit_new_payee_card.yaml +13 -0
  32. scamgate-0.3.3/scenarios/pay_09_legit_new_landlord_vop_match.yaml +15 -0
  33. scamgate-0.3.3/scenarios/pay_10_new_payee_transfer_no_vop.yaml +13 -0
  34. scamgate-0.3.3/scenarios/url_01_fake_shop_lookalike.yaml +8 -0
  35. scamgate-0.3.3/scenarios/url_02_paypal_typosquat_login.yaml +8 -0
  36. scamgate-0.3.3/scenarios/url_03_hotel_seed_phrase.yaml +8 -0
  37. scamgate-0.3.3/scenarios/url_04_punycode_homoglyph.yaml +9 -0
  38. scamgate-0.3.3/scenarios/url_05_ip_host_bank.yaml +8 -0
  39. scamgate-0.3.3/scenarios/url_06_blocklisted.yaml +8 -0
  40. scamgate-0.3.3/scenarios/url_07_legit_hotel_booking.yaml +8 -0
  41. scamgate-0.3.3/scenarios/url_08_legit_revolut_login.yaml +8 -0
  42. scamgate-0.3.3/scenarios/url_09_userinfo_disguise.yaml +8 -0
  43. scamgate-0.3.3/scenarios/url_10_official_domain_prefix.yaml +8 -0
  44. scamgate-0.3.3/scenarios/url_11_unknown_site_login.yaml +8 -0
  45. scamgate-0.3.3/scenarios/url_12_legit_applebees.yaml +8 -0
  46. scamgate-0.3.3/scenarios/url_13_backslash_disguise.yaml +8 -0
  47. scamgate-0.3.3/scenarios/url_14_data_link_sign_in.yaml +8 -0
  48. scamgate-0.3.3/scripts/claude_plugin_smoke.py +111 -0
  49. scamgate-0.3.3/scripts/fetch_feeds.py +6 -0
  50. scamgate-0.3.3/scripts/plugin_smoke.py +101 -0
  51. scamgate-0.3.3/scripts/site_demo_data.py +116 -0
  52. scamgate-0.3.3/server.json +46 -0
  53. scamgate-0.3.3/site/.nojekyll +0 -0
  54. scamgate-0.3.3/site/assets/demo-data.js +501 -0
  55. scamgate-0.3.3/site/assets/favicon.svg +6 -0
  56. scamgate-0.3.3/site/assets/fonts/IBMPlexMono-400.woff2 +0 -0
  57. scamgate-0.3.3/site/assets/fonts/IBMPlexMono-500.woff2 +0 -0
  58. scamgate-0.3.3/site/assets/fonts/IBMPlexSans-var.woff2 +0 -0
  59. scamgate-0.3.3/site/assets/fonts/InstrumentSerif-400-italic.woff2 +0 -0
  60. scamgate-0.3.3/site/assets/fonts/InstrumentSerif-400.woff2 +0 -0
  61. scamgate-0.3.3/site/assets/fonts/OFL.txt +190 -0
  62. scamgate-0.3.3/site/assets/site.js +158 -0
  63. scamgate-0.3.3/site/assets/style.css +308 -0
  64. scamgate-0.3.3/site/docs.html +451 -0
  65. scamgate-0.3.3/site/index.html +190 -0
  66. scamgate-0.3.3/site/privacy.html +126 -0
  67. scamgate-0.3.3/src/scamgate/__init__.py +11 -0
  68. scamgate-0.3.3/src/scamgate/asklog.py +153 -0
  69. scamgate-0.3.3/src/scamgate/bench.py +77 -0
  70. scamgate-0.3.3/src/scamgate/cli.py +209 -0
  71. scamgate-0.3.3/src/scamgate/config/rules.yaml +218 -0
  72. scamgate-0.3.3/src/scamgate/config.py +62 -0
  73. scamgate-0.3.3/src/scamgate/data/blocklist.txt +9 -0
  74. scamgate-0.3.3/src/scamgate/domainage.py +153 -0
  75. scamgate-0.3.3/src/scamgate/engine.py +145 -0
  76. scamgate-0.3.3/src/scamgate/entry.py +19 -0
  77. scamgate-0.3.3/src/scamgate/feeds.py +377 -0
  78. scamgate-0.3.3/src/scamgate/guard.py +116 -0
  79. scamgate-0.3.3/src/scamgate/harden.py +393 -0
  80. scamgate-0.3.3/src/scamgate/hook.py +1055 -0
  81. scamgate-0.3.3/src/scamgate/hook_entry.py +15 -0
  82. scamgate-0.3.3/src/scamgate/hostnorm.py +60 -0
  83. scamgate-0.3.3/src/scamgate/integrity.py +85 -0
  84. scamgate-0.3.3/src/scamgate/models.py +77 -0
  85. scamgate-0.3.3/src/scamgate/overrides.py +90 -0
  86. scamgate-0.3.3/src/scamgate/payees.py +141 -0
  87. scamgate-0.3.3/src/scamgate/paygate.py +228 -0
  88. scamgate-0.3.3/src/scamgate/plugin.py +222 -0
  89. scamgate-0.3.3/src/scamgate/plugin.yaml +24 -0
  90. scamgate-0.3.3/src/scamgate/redact.py +93 -0
  91. scamgate-0.3.3/src/scamgate/refresh.py +233 -0
  92. scamgate-0.3.3/src/scamgate/registry.py +66 -0
  93. scamgate-0.3.3/src/scamgate/rules/__init__.py +1 -0
  94. scamgate-0.3.3/src/scamgate/rules/payment.py +127 -0
  95. scamgate-0.3.3/src/scamgate/rules/url.py +446 -0
  96. scamgate-0.3.3/src/scamgate/server.py +75 -0
  97. scamgate-0.3.3/src/scamgate/state.py +32 -0
  98. scamgate-0.3.3/src/scamgate/tools.py +94 -0
  99. scamgate-0.3.3/tests/conftest.py +64 -0
  100. scamgate-0.3.3/tests/fixtures/feeds/openphish.txt +3 -0
  101. scamgate-0.3.3/tests/fixtures/feeds/phishing-domains.txt +4 -0
  102. scamgate-0.3.3/tests/fixtures/feeds/urlhaus.csv +5 -0
  103. scamgate-0.3.3/tests/test_bench_and_server.py +108 -0
  104. scamgate-0.3.3/tests/test_brand_pack.py +61 -0
  105. scamgate-0.3.3/tests/test_claude_plugin.py +318 -0
  106. scamgate-0.3.3/tests/test_domain_age.py +113 -0
  107. scamgate-0.3.3/tests/test_feeds.py +207 -0
  108. scamgate-0.3.3/tests/test_final_review.py +164 -0
  109. scamgate-0.3.3/tests/test_guard_boundaries.py +148 -0
  110. scamgate-0.3.3/tests/test_hermes_holes.py +298 -0
  111. scamgate-0.3.3/tests/test_hook.py +320 -0
  112. scamgate-0.3.3/tests/test_hook_holes.py +329 -0
  113. scamgate-0.3.3/tests/test_hook_perf.py +184 -0
  114. scamgate-0.3.3/tests/test_hook_perf_full.py +93 -0
  115. scamgate-0.3.3/tests/test_hook_shell_words.py +122 -0
  116. scamgate-0.3.3/tests/test_integrity.py +31 -0
  117. scamgate-0.3.3/tests/test_payees_and_review.py +145 -0
  118. scamgate-0.3.3/tests/test_paygate.py +187 -0
  119. scamgate-0.3.3/tests/test_payment_rules.py +82 -0
  120. scamgate-0.3.3/tests/test_plugin.py +132 -0
  121. scamgate-0.3.3/tests/test_plugin_feed_updates.py +280 -0
  122. scamgate-0.3.3/tests/test_recheck_gaps.py +234 -0
  123. scamgate-0.3.3/tests/test_redact.py +29 -0
  124. scamgate-0.3.3/tests/test_site_demo.py +43 -0
  125. scamgate-0.3.3/tests/test_tamper_training.py +351 -0
  126. scamgate-0.3.3/tests/test_update_feeds_cli.py +45 -0
  127. scamgate-0.3.3/tests/test_url_rules.py +304 -0
  128. scamgate-0.3.3/uv.lock +979 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "scamgate",
3
+ "description": "scamgate for Claude Code: a hook that checks every tool call for scam links and risky payments, plus link and payment check tools.",
4
+ "owner": {
5
+ "name": "Odai Ameera",
6
+ "url": "https://github.com/odaiameera"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "scamgate",
11
+ "source": "./claude-plugin",
12
+ "description": "Blocks known-bad links and risky payments before a tool runs, and asks you when it can't see where a command is going.",
13
+ "version": "0.3.3",
14
+ "category": "security",
15
+ "homepage": "https://odaiameera.github.io/scamgate/"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,91 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ name: Tests (Python ${{ matrix.python-version }})
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 10
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.11", "3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ enable-cache: true
34
+
35
+ - name: Install
36
+ run: uv sync --locked
37
+
38
+ - name: Unit tests
39
+ run: uv run --locked pytest -q
40
+
41
+ - name: Scenario bench (must match expected decisions)
42
+ run: uv run --locked scamgate bench --strict
43
+
44
+ - name: Feed update on sample data (scamgate update-feeds, feeds-status)
45
+ run: uv run --locked pytest -q tests/test_update_feeds_cli.py tests/test_plugin_feed_updates.py
46
+
47
+ plugin-install:
48
+ name: Hermes plugin install (no typer, sample feeds)
49
+ runs-on: ubuntu-latest
50
+ timeout-minutes: 10
51
+ steps:
52
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
53
+ with:
54
+ persist-credentials: false
55
+
56
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
57
+ with:
58
+ python-version: "3.11"
59
+
60
+ - name: Bare venv with only the plugin's python_dependencies
61
+ run: |
62
+ uv venv "$RUNNER_TEMP/bare"
63
+ uv pip install --python "$RUNNER_TEMP/bare" "pydantic>=2.7,<3" "pyyaml>=6.0,<7"
64
+ mkdir -p "$RUNNER_TEMP/plugins"
65
+ cp -r src/scamgate "$RUNNER_TEMP/plugins/scamgate"
66
+
67
+ - name: hermes scamgate update-feeds, /scamgate update and the update tool
68
+ run: '"$RUNNER_TEMP/bare/bin/python" scripts/plugin_smoke.py "$RUNNER_TEMP/plugins" tests/fixtures/feeds'
69
+
70
+ claude-plugin:
71
+ name: Claude Code plugin (validate, hook and server from the wheel)
72
+ runs-on: ubuntu-latest
73
+ timeout-minutes: 15
74
+ steps:
75
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76
+ with:
77
+ persist-credentials: false
78
+
79
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
80
+ with:
81
+ python-version: "3.11"
82
+
83
+ - name: claude plugin validate (plugin and marketplace)
84
+ run: |
85
+ npx -y @anthropic-ai/claude-code@2.1.310 plugin validate ./claude-plugin --strict
86
+ npx -y @anthropic-ai/claude-code@2.1.310 plugin validate . --strict
87
+
88
+ - name: The plugin's hook and serve commands, run from the built wheel through uvx
89
+ run: |
90
+ uv build --out-dir "$RUNNER_TEMP/dist"
91
+ uv run --no-project python scripts/claude_plugin_smoke.py "$RUNNER_TEMP"/dist/scamgate-*-py3-none-any.whl
@@ -0,0 +1,35 @@
1
+ name: Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths: ["site/**", ".github/workflows/pages.yml"]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: pages
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ deploy:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ permissions:
21
+ pages: write
22
+ id-token: write
23
+ environment:
24
+ name: github-pages
25
+ url: ${{ steps.deployment.outputs.page_url }}
26
+ steps:
27
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28
+ with:
29
+ persist-credentials: false
30
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
31
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
32
+ with:
33
+ path: site
34
+ - id: deployment
35
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
@@ -0,0 +1,114 @@
1
+ name: release
2
+
3
+ # Runs when a GitHub release is published. Builds the wheel and sdist once, checks them, then publishes the
4
+ # same files to PyPI (trusted publishing, no token), attaches them to the GitHub release with SHA256SUMS.txt,
5
+ # and lists the new version in the MCP Registry (GitHub OIDC, no token).
6
+ #
7
+ # One-time setup on pypi.org: a trusted publisher for project "scamgate", owner "odaiameera", repository
8
+ # "scamgate", workflow "release.yml", environment "pypi".
9
+
10
+ on:
11
+ release:
12
+ types: [published]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ concurrency:
18
+ group: release-${{ github.event.release.tag_name }}
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 20
25
+ steps:
26
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ with:
28
+ persist-credentials: false
29
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
30
+ - name: The tag names the package version everywhere
31
+ env:
32
+ TAG: ${{ github.event.release.tag_name }}
33
+ run: |
34
+ v=$(uv version --short)
35
+ test "v$v" = "$TAG" || { echo "tag $TAG doesn't match pyproject version $v"; exit 1; }
36
+ grep -q "\"version\": \"$v\"" server.json
37
+ grep -q "\"version\": \"$v\"" claude-plugin/.claude-plugin/plugin.json
38
+ grep -q "uvx scamgate==$v hook" claude-plugin/hooks/hooks.json
39
+ grep -q "\"scamgate==$v\"" claude-plugin/.mcp.json
40
+ - name: Build
41
+ run: uv build --out-dir dist
42
+ - name: Check the built wheel the way the plugin runs it
43
+ run: |
44
+ uv run --no-project python scripts/claude_plugin_smoke.py dist/scamgate-*-py3-none-any.whl
45
+ uvx --from dist/scamgate-*-py3-none-any.whl scamgate bench --strict
46
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ if-no-files-found: error
51
+
52
+ pypi:
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ timeout-minutes: 15
56
+ environment:
57
+ name: pypi
58
+ url: https://pypi.org/project/scamgate/
59
+ permissions:
60
+ id-token: write
61
+ steps:
62
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
67
+
68
+ github-release:
69
+ needs: pypi
70
+ runs-on: ubuntu-latest
71
+ timeout-minutes: 10
72
+ permissions:
73
+ contents: write
74
+ steps:
75
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+ - name: Attach the published files and their checksums
80
+ env:
81
+ GH_TOKEN: ${{ github.token }}
82
+ TAG: ${{ github.event.release.tag_name }}
83
+ run: |
84
+ cd dist
85
+ sha256sum scamgate-* > SHA256SUMS.txt
86
+ gh release upload "$TAG" scamgate-* SHA256SUMS.txt --clobber --repo "$GITHUB_REPOSITORY"
87
+
88
+ mcp-registry:
89
+ needs: pypi
90
+ runs-on: ubuntu-latest
91
+ timeout-minutes: 15
92
+ permissions:
93
+ id-token: write
94
+ steps:
95
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
96
+ with:
97
+ persist-credentials: false
98
+ - name: Install mcp-publisher 1.8.4 (checksum-verified)
99
+ run: |
100
+ curl -fsSL -o mcp-publisher.tar.gz \
101
+ https://github.com/modelcontextprotocol/registry/releases/download/v1.8.4/mcp-publisher_linux_amd64.tar.gz
102
+ echo "5dc2fd415369c73551259f31cd33e2aacc9e0d1021e7f08fef1492d93b781628 mcp-publisher.tar.gz" | sha256sum -c -
103
+ tar xzf mcp-publisher.tar.gz mcp-publisher
104
+ - name: Wait until PyPI serves this version
105
+ env:
106
+ TAG: ${{ github.event.release.tag_name }}
107
+ run: |
108
+ for i in $(seq 1 30); do
109
+ curl -fsS "https://pypi.org/pypi/scamgate/${TAG#v}/json" > /dev/null && exit 0
110
+ sleep 10
111
+ done
112
+ echo "PyPI doesn't list scamgate ${TAG#v} yet"; exit 1
113
+ - run: ./mcp-publisher login github-oidc
114
+ - run: ./mcp-publisher publish
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .env
11
+ .env.*
12
+ # Downloaded third-party feeds (fetched locally, never committed)
13
+ data/feeds/
@@ -0,0 +1,66 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for coding agents working on scamgate.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ uv sync # install deps (incl. dev group)
9
+ uv run pytest -q # all tests must pass (offline)
10
+ uv run scamgate bench --strict # scenarios must match expected decisions
11
+ uv run scamgate bench -v # show reasons per scenario
12
+ uv run scamgate check-url URL --intent "..."
13
+ uv run scamgate serve # MCP server over stdio
14
+ uv run python scripts/site_demo_data.py # regenerate site verdicts after rule/weight changes
15
+ ```
16
+
17
+ If a live `scamgate serve` holds `.venv` (Windows locks the exe), use a separate venv:
18
+ `UV_PROJECT_ENVIRONMENT=/some/other/venv uv run pytest`.
19
+
20
+ ## Layout
21
+
22
+ - `models.py`: input models and the `Verdict` (decision, risk_score, reasons)
23
+ - `registry.py`: `@register(rule_id, kind)` rule registry
24
+ - `rules/url.py`, `rules/payment.py`: rules; `config/rules.yaml`: weights, thresholds, word lists
25
+ - `engine.py`: runs rules, scores, decides; payee facts come from history, not the caller
26
+ - `feeds.py`: known-bad-link feeds. Only `update_feeds()` touches the network; checks read the cache
27
+ (`LazyFeedIndex` for the hook)
28
+ - `refresh.py`: argument-free feed refresh (lock file, age checks) behind the Hermes plugin's
29
+ `/scamgate update`, `scamgate_update_feeds` tool, `hermes scamgate ...` CLI and background refresh.
30
+ Needs only the plugin's dependencies (no typer/rich)
31
+ - `hostnorm.py`: host canonicalisation (percent-decoding, numeric IPv4 forms)
32
+ - `hook.py` / `hook_entry.py`: the enforcement hook (Hermes + Claude Code dialects)
33
+ - `guard.py`: human-only writes (`require_human`) and HMAC signing of state records
34
+ - `overrides.py`, `payees.py`, `asklog.py`, `state.py`: signed overrides, salted payee history,
35
+ redacted ask log, state dir; `redact.py`: secret stripping; `integrity.py`: install self-check
36
+ - `server.py`: MCP server; `cli.py`: Typer CLI
37
+ - `scenarios/*.yaml`: example scenarios; `src/scamgate/data/blocklist.txt`: example blocklist
38
+ - `site/`: GitHub Pages site; `docs/design/`: design notes and review write-ups
39
+
40
+ ## Rules of the road
41
+
42
+ - Deterministic and offline: no network calls or LLMs in the check path. Only feed refreshes download,
43
+ always the default feeds from fixed URLs; nothing an agent can call picks the feeds, URLs or cache folder.
44
+ Tests never touch the network.
45
+ - The Hermes plugin must work with only `plugin.yaml`'s `python_dependencies` installed: no typer, rich or
46
+ mcp imports on the plugin path (`scripts/plugin_smoke.py` checks this in CI).
47
+ - Every rule needs a `rules.yaml` entry, tests, a scenario where it makes sense, and a plain-English
48
+ explanation a non-technical person can act on.
49
+ - The hook never outputs "allow". It may only block or ask; silence means no objection.
50
+ - The hook fails closed on pre events. New detection must not add false positives on everyday dev
51
+ commands (`git push`, `pip install -r`, `uv sync`, `npm ci/test`, `grep`): add a quiet-path test.
52
+ - Never let agent-supplied text lower a score (stated intent, `is_new_payee`, reasons): it can only add risk.
53
+ - Overrides and payees are written only by a person (`guard.require_human`) and are signed. Don't add
54
+ any code path that writes them without that check.
55
+ - Write a failing test for every bypass before fixing it (`tests/test_hook_holes.py`,
56
+ `test_tamper_training.py`, `test_recheck_gaps.py`, `test_final_review.py`).
57
+ - Never bundle third-party blocklist data; feeds live in the cache dir.
58
+ - No invented statistics in docs. Bench numbers come from example scenarios only.
59
+ - Keep the advisory framing: scamgate flags risk; it doesn't certify safety.
60
+
61
+ ## Website
62
+
63
+ - `site/` is static HTML/CSS/JS (no build step), deployed by `.github/workflows/pages.yml` on pushes
64
+ to `main` that touch `site/`. Pages source must stay "GitHub Actions", not "Deploy from branch".
65
+ - Verdicts on the site come from `site/assets/demo-data.js`, generated by
66
+ `scripts/site_demo_data.py`. `tests/test_site_demo.py` fails if it's stale. Never hand-edit verdicts.
@@ -0,0 +1,245 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.3.3] - 2026-10-26
6
+
7
+ scamgate as a Claude Code plugin, and on PyPI. One install in Claude Code brings the enforcing hook, the
8
+ link and payment check tools, and a refresh of the phishing lists. The plugin runs scamgate from PyPI with
9
+ `uvx scamgate==0.3.3`, pinned to one version.
10
+
11
+ ### Claude Code plugin
12
+ - New `claude-plugin/` with a `SessionStart`, `PreToolUse` (fails closed), `PostToolUse` and
13
+ `PostToolUseFailure` hook and the MCP server, and a marketplace in `.claude-plugin/marketplace.json`:
14
+ `/plugin install scamgate --marketplace odaiameera/scamgate`. Settings: `auto_update_feeds` (on) and an
15
+ optional URLhaus key, kept in the system's credential store. `claude-plugin/README.md` lists what it
16
+ runs, downloads and stores.
17
+ - Tested in Claude Code 2.1.310: the hook blocked a listed host before `curl` ran, `echo` ran without a
18
+ prompt, `check_url` and `update_feeds` worked, and lists 8 hours old refreshed during the session.
19
+ - `scamgate hook` goes straight to the hook without loading the command line (about 0.8 s per checked
20
+ tool call on a Windows PC, with `uvx` adding almost nothing).
21
+
22
+ ### MCP server
23
+ - New `update_feeds` tool, the same no-argument refresh as the Hermes plugin's: default lists from fixed
24
+ sources, at most once an hour.
25
+ - `scamgate serve` started with `SCAMGATE_AUTO_UPDATE=1` refreshes the lists by itself when they are 6
26
+ hours old. It stays off unless set, so a hand-configured server downloads nothing unasked.
27
+ - An unset or unfilled URLhaus key (empty, or a `${...}` placeholder) now means "no key" instead of a
28
+ download address with the placeholder in it.
29
+
30
+ ### Tamper guard
31
+ - Protects what a Claude Code install adds: `~/.claude/plugins` (the installed plugin's own hooks), project
32
+ and user `settings.local.json`, and the same files under `CLAUDE_CONFIG_DIR`. Reading them stays quiet.
33
+ - Blocks agent commands that disable or remove a Claude Code plugin or its marketplace, or start Claude in
34
+ bare mode (which skips every hook), and asks before an agent installs, enables or configures a plugin or
35
+ starts Claude with other settings sources.
36
+ - The plugin's own check tools (`mcp__plugin_scamgate_scamgate__check_url`, `..._check_payment`) may name a
37
+ listed link, like the other spellings of scamgate's tools.
38
+
39
+ ### Packaging
40
+ - Published to PyPI as `scamgate`, from a GitHub Actions workflow with trusted publishing; the release
41
+ workflow also attaches the same wheel and sdist with `SHA256SUMS.txt`, and lists the server in the MCP
42
+ Registry as `io.github.odaiameera/scamgate` (`server.json`).
43
+ - Project URLs, keywords and classifiers in `pyproject.toml`. Install commands use `scamgate==0.3.3`.
44
+ - New CI job: `claude plugin validate` on the plugin and the marketplace, and the plugin's hook and server
45
+ commands run from the built wheel (`scripts/claude_plugin_smoke.py`).
46
+
47
+ ### Docs
48
+ - README banner and badge row, notes on #21, #22 and #24, and thanks to @simin-yuan (#25).
49
+ - New `CONTRIBUTING.md` and `SECURITY.md`.
50
+
51
+ ## [0.3.2] - 2026-10-26
52
+
53
+ Feed refreshes that work on a plain Hermes plugin install, fewer false blocks from the tamper guard,
54
+ and a hook that stays fast on very long commands (PRs #21, #22, #23 and #24). Install commands are pinned
55
+ to `v0.3.2`. Reported from a real install, where `/scamgate update` told people to run a
56
+ `scamgate update-feeds` command a plugin install doesn't have.
57
+
58
+ ### Speed on long commands (#20)
59
+ - A 100,000-character command kept the hook busy for about 40 s, and the hook accepts up to 2 MiB.
60
+ The call-looking check is now one linear pass and each segment is split once (#21), and a one-pass
61
+ reader returns exactly the words `shlex` would (#22), both by @simin-yuan. The script check in
62
+ `harden.py` now uses the same reader (#24), so the whole hook call, not just `extract()`, is linear:
63
+ a 2,000,000-character quoted command went from over 200 s to about 5 s on one Windows machine.
64
+ - No decision changed: 11,350 commands (every string in the test suites plus quoting and spacing
65
+ variants around listed hosts) gave the same decision and message before and after #21 and #22.
66
+ - The tamper guard's read-only check reads only each segment's first word, without `shlex`, and runs
67
+ only after a write or feed-call pattern matched.
68
+
69
+ ### Hermes plugin
70
+ - `/scamgate update` now downloads the phishing lists instead of pointing at a missing command, and
71
+ `/scamgate status` lists each feed with its age.
72
+ - New `hermes scamgate update-feeds` and `hermes scamgate feeds-status`. They use Hermes' own command line, so
73
+ they need no extra packages (no typer, no `scamgate` on `PATH`).
74
+ - New agent tool `scamgate_update_feeds`. It takes no arguments: it always downloads the default feeds from
75
+ their fixed sources into the configured cache, and does nothing if they were refreshed in the last hour.
76
+ An agent can ask for a refresh, but can't choose what lands in the cache.
77
+ - Background refresh: about two minutes after Hermes starts, then every 6 hours. A lock file keeps
78
+ Hermes processes from downloading at the same time. Off with `auto_update_feeds: false` or
79
+ `SCAMGATE_AUTO_UPDATE=0`. No cron line needed.
80
+ - Stale-list warnings now name the plugin commands as well as `scamgate update-feeds`.
81
+
82
+ ### Tamper guard
83
+ - A path in a command only counts when it names the protected folder itself: a log file next to the feed
84
+ cache folder (`scamgate-feeds.log`) is no longer treated as inside it. `./`, `//` and `..` spellings of a
85
+ protected path are now caught too.
86
+ - Read-only commands on protected paths pass: `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `find`, `stat`,
87
+ `which`, `wc`, `du`, `sha256sum` (and a few more), with `2>/dev/null` or `2>&1` allowed. A redirect,
88
+ `$(...)`, backticks, `-exec`, `-delete`, `-fprint`, `--pre` or any other command in the line makes it a
89
+ write check again, as before.
90
+ - Commands that call the feed downloader with their own arguments (`update_feeds(fetch=...)`,
91
+ `_atomic_write`, swapping the downloader) are blocked: use the update tool or `/scamgate update`.
92
+
93
+ ### Tests and CI
94
+ - New `tests/test_guard_boundaries.py` (quiet reads, the log file next to the cache, writes that must still
95
+ block, cache poisoning), `tests/test_plugin_feed_updates.py` and `tests/test_update_feeds_cli.py`.
96
+ - CI runs `update-feeds` and `feeds-status` on sample feed data, and a new `plugin-install` job runs
97
+ `scripts/plugin_smoke.py` in a venv that has only the plugin's dependencies.
98
+ - `tests/test_hook_perf.py` (#21, #22) and `tests/test_hook_perf_full.py` (#24) time the hook in a
99
+ subprocess with a hard timeout; the second times the whole `hook.run`, not just `extract()`.
100
+
101
+ ## [0.3.1] - 2026-10-22
102
+
103
+ Security fixes from two review rounds (PRs #17 and #18). Install commands are pinned to `v0.3.1`.
104
+
105
+ ### Link checks
106
+ - New rule `url.unreadable` (40, asks): a link scamgate can't read never gets `allow`. That covers links
107
+ that don't parse, site names with characters a browser refuses (spaces, slashes), site names longer than
108
+ DNS allows, addresses over 8 KiB, and `javascript:`, `data:`, `vbscript:`, `file:` and `blob:` links.
109
+ No other rule runs on such a link, so its text never appears in a reason.
110
+ - `\` before the query is read as `/`, as browsers do: `https://a.example\.paypal.com/` is checked as
111
+ `a.example`, not as a PayPal address.
112
+ - Ideographic and halfwidth full stops (`。`, `。`) in a site name count as dots.
113
+ - Site names over 253 characters are rejected before the brand checks run (a 24 KB one took 37 s).
114
+ - Hook: `http://[::1]:3000/` is read as one link (the address used to be cut at `]`).
115
+ - `amazonaws.com` is no longer an official Amazon domain. Its subdomains are customers' buckets and
116
+ servers (583 Public Suffix List entries), so a sign-in page there is judged like any other site, and a
117
+ feed listing of one bucket's host counts.
118
+ - A feed listing of a shared suffix such as `s3.amazonaws.com` no longer condemns every site under it:
119
+ a parent-domain listing counts only at or below the site's own registrable domain.
120
+ - Three-label shared suffixes in the curated list (`s3.amazonaws.com`) now take effect. Only the last two
121
+ labels used to be compared, so each bucket was treated as part of `amazonaws.com`.
122
+ - New `lists.shared_hosting` in `rules.yaml` (`amazonaws.com`, `googleusercontent.com`, `cloudapp.azure.com`):
123
+ a host under one of these is a customer's or user's own site, so it's never an official brand site, and a
124
+ feed listing counts only for that exact host. `googleusercontent.com` is no longer an official Google
125
+ domain (Google uses it to sandbox untrusted user content, such as Apps Script web apps). `azure.com` stays
126
+ official for Microsoft's own portal; only the VM names customers pick under `cloudapp.azure.com` are carved out.
127
+ - Faster link checks, same verdicts: the brand-padding search is linear (a 12,000-character word went from
128
+ 7.5 s to 0.03 s), the typo check skips words whose length rules out a one-letter edit, each link is
129
+ parsed once per check, and each word's look-alike variants are worked out once instead of once per brand.
130
+ At the 253-character cap the slowest link went from 23 ms to 1.4 ms. Verdicts on 16,050 generated
131
+ brand-shaped links are unchanged.
132
+ - The fake-address check (`paypal.com@evil.example`) now fires however the scheme is written:
133
+ `https:/…`, `https:\\…` and tabs or newlines inside the scheme reach the same host as `https://…`, so
134
+ they're judged the same. A domain or brand before the `@` counts as a disguise on any scheme
135
+ (`sftp://`, `git+https://`, `hxxps://`); a plain username is flagged only on web links, so
136
+ `ssh://git@github.com/…` no longer asks.
137
+
138
+ ### Hook
139
+ - Shell commands are read the way the shell passes them: quotes and escapes are removed first, and each
140
+ word that is a link is checked whole. A quoted `@` or `)` can no longer cut a link short and hide its
141
+ real host.
142
+ - `curl "https://evil""shop.com/"` is now checked as `https://evilshop.com/` (it used to ask as a link
143
+ built at run time). Variables and `$(…)` still ask.
144
+ - A network command the shell can't split (an unbalanced quote) asks.
145
+ - Bash `/dev/tcp/HOST/PORT` and `/dev/udp/HOST/PORT` are checked like any other destination.
146
+ - Only scamgate's own check tools skip the gate (`mcp__scamgate__check_url`, `mcp_scamgate_check_url`,
147
+ `scamgate_check_url` and the `check_payment` forms). A tool whose name merely contains them, like
148
+ `mcp__notscamgate__check_url`, used to skip every check, tamper checks included. If you registered the
149
+ MCP server under another name, asking it about a bad link may now ask you first.
150
+
151
+ ### Local state
152
+ - The state dir is created private (0700), and an existing `…/scamgate` state dir from an older install is
153
+ tightened to 0700. A custom `SCAMGATE_STATE_DIR` is left as you set it. The ask log (`asks.jsonl`,
154
+ `outcomes.jsonl`) is written 0600: it shows which sites your agent tried to visit.
155
+ - The signing key and payee salt are created once, even when two scamgate processes start together. One
156
+ used to overwrite the other's key, and records already signed with it stopped verifying.
157
+ - If override rules can't be verified (the signing key is missing or was changed, or a line was edited),
158
+ checks now warn instead of dropping your deny rules silently.
159
+
160
+ ### Input checks
161
+ - Payment `amount` and `budget` must be real numbers: `inf`, `nan` and a negative budget are rejected.
162
+ - In the hook, a payment tool call whose amount is JSON `Infinity` or `NaN`, or a digit string too long
163
+ to be a number, asks ("can't read how much") instead of failing with an internal error.
164
+ - The Hermes plugin fails closed: if the gate itself errors on a pre-tool call, the call is blocked, as
165
+ the shell hook already did.
166
+ - Domain-age lookups (`SCAMGATE_RDAP=live`) send only a plain host name to the registry, and look up
167
+ international names in their `xn--` form (they used to fail). A name Python's older IDNA rules would
168
+ change (`faß` to `fass`) isn't looked up at all, rather than looked up wrong.
169
+ - `update-feeds` refuses a feed download over 256 MB and keeps the old cache.
170
+
171
+ ### MCP server and Hermes plugin
172
+ - Both MCP tools are marked read-only (`readOnlyHint`, not destructive, idempotent). `check_url` reports
173
+ `openWorldHint` only when domain-age lookups go online (`SCAMGATE_RDAP=live`).
174
+ - Both tools return a typed `Verdict` with a published output schema, so clients get structured content.
175
+ - New `checked_host` in every link verdict: the host a browser would open. Empty for payments and for
176
+ links scamgate can't read.
177
+ - Input limits on the MCP tools (a 64 KiB link, 2,000-character intent, 20,000-character context). A link
178
+ over 8 KiB but under the limit still gets a verdict (`url.unreadable` asks) rather than an error.
179
+ - The Hermes plugin's tools now take the same arguments as the MCP tools, from one shared definition
180
+ (`scamgate.tools`). `scamgate_check_payment` used to drop `budget`, `is_new_payee`,
181
+ `bank_details_changed` and `vop_result`, so the over-budget check never ran there. Bad arguments come
182
+ back as an error instead of crashing the tool.
183
+ - Hermes `/scamgate review` lists each host with how many times it asked, what you did, and the top rule.
184
+ It used to print a line of `?` per host.
185
+
186
+ ## [0.3.0] - 2026-10-16
187
+
188
+ Hermes plugin, and the holes found while making it.
189
+
190
+ ### Hermes plugin
191
+ - In-process plugin (`src/scamgate/plugin.yaml`): `pre_tool_call`, `post_tool_call`, `scamgate_check_url`, `scamgate_check_payment`, `/scamgate`.
192
+ - Same decisions as the shell hook, without a subprocess. Needs Hermes 0.19.1+.
193
+ - When `/yolo` or `approvals.mode: off` would auto-approve an ask, the plugin blocks instead. Set `plugins.entries.scamgate.settings.when_approvals_off: allow` to keep the old behaviour.
194
+
195
+ ### Hook
196
+ - Blocks Hermes safety switches (`approvals.mode off`, `--yolo`, `plugins uninstall`, `--safe-mode`, `--dangerously-skip-permissions`, and the same idea in code or another tool).
197
+ - Blocks reading the signing key and payee salt.
198
+ - Reads a script before running it, including scripts a cron job would run later with no agent.
199
+ - Checks the body of files other tools run (Makefile, package.json, git hooks, CI, Hermes cron scripts). Notes are still just notes.
200
+ - scamgate's own check tools are not gated for naming the link they were asked about.
201
+ - Other Hermes profiles' config is protected.
202
+ - `https:/host` and `https:///host` are checked as `https://host`.
203
+ - `scp`, `sftp` and `rsync` to a blocklisted host are denied, including `user@host:/absolute/path`. Local copies stay quiet.
204
+ - `base32 -d`, `b64 -d`, a decoded file that is then run, and Perl or Ruby `eval` of decoded text ask before they run.
205
+
206
+ Install commands are pinned to `v0.3.0`.
207
+
208
+ ## [0.2.0] - 2026-10-12
209
+
210
+ First tagged release. Everything since 0.1.0 (the MCP server and rules) is new here.
211
+
212
+ ### Enforcement hook
213
+ - `scamgate-hook`: a Hermes `pre_tool_call` / Claude Code `PreToolUse` hook, so the agent can't skip
214
+ the check. Never outputs "allow"; fails closed; asks are logged (redacted) with the outcome.
215
+ - Links in any tool call are checked, including disguised hosts (percent-encoding, integer/hex/octal
216
+ IPs), per command segment, so one clean link can't hide a second fetch.
217
+ - Destinations it can't read (variables, `$(…)`, string-built, base64/hex-decoded links, named
218
+ package installs, new git remotes) go to a human. Decoded-and-run code (`base64 -d | bash`,
219
+ `exec(b64decode(…))`, `powershell -enc`) always does.
220
+ - **Payments are gated**: money-moving tool calls are scored with `check_payment` and local payee
221
+ history; payments in shell/code (crypto CLIs, payment-API writes) go to a human.
222
+ - Routine dev commands stay quiet (lockfile installs, `git push/pull`, text-only commands).
223
+ - Tamper resistance: agent writes to scamgate, its state, feed cache, venv and the Claude/Hermes hook
224
+ config are blocked however the path is spelled; integrity check against wheel RECORD hashes and
225
+ unknown `.pth` files.
226
+
227
+ ### Human-only state
228
+ - `review` / `promote` overrides and `record-payment` payee history: written only by a person at an
229
+ interactive terminal (refused inside known agents), HMAC-signed, salted hashes for payees,
230
+ near-name and account-reuse detection, salt-swap detection.
231
+
232
+ ### Rules
233
+ - `url.credential_page`: a sign-in page on an unrecognised site asks; the agent's wording can't clear it.
234
+ - `url.domain_age`: RDAP domain age, **opt-in** (`SCAMGATE_RDAP=cache|live`), cached.
235
+ - Brand pack of ~50 commonly phished brands plus Irish/UK banks and couriers; brand-inside-a-word
236
+ false positives fixed (applebees, revolution, pineapple); curated public-suffix list incl. shared hosts.
237
+ - New-payee bank transfers ask unless Verification of Payee matched; `pay.unbacked_known_claim`.
238
+ - 0.0.0.0 / loopback treated as local.
239
+
240
+ ### Docs and site
241
+ - GitHub Pages site and docs; Claude Code config uses `"onFailure": "block"`; install commands pinned to `v0.2.0`.
242
+
243
+ ## [0.1.0]
244
+
245
+ - MCP server with `check_url` and `check_payment`, YAML-configured rules, known-bad-link feeds, bench scenarios.