harpo-scan 1.4.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.
- harpo_scan-1.4.0/LICENSE +28 -0
- harpo_scan-1.4.0/PKG-INFO +494 -0
- harpo_scan-1.4.0/README.md +449 -0
- harpo_scan-1.4.0/cli/Harpocrates/__init__.py +15 -0
- harpo_scan-1.4.0/cli/Harpocrates/allowlist.py +743 -0
- harpo_scan-1.4.0/cli/Harpocrates/audit.py +386 -0
- harpo_scan-1.4.0/cli/Harpocrates/cli/__init__.py +861 -0
- harpo_scan-1.4.0/cli/Harpocrates/cli/__main__.py +5 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/__init__.py +0 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/classification.py +512 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/detector.py +1176 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/result.py +258 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/rust_backend.py +651 -0
- harpo_scan-1.4.0/cli/Harpocrates/core/scanner.py +430 -0
- harpo_scan-1.4.0/cli/Harpocrates/deprecated.py +54 -0
- harpo_scan-1.4.0/cli/Harpocrates/detectors/__init__.py +15 -0
- harpo_scan-1.4.0/cli/Harpocrates/detectors/entropy_detector.py +67 -0
- harpo_scan-1.4.0/cli/Harpocrates/detectors/path_tokens.py +121 -0
- harpo_scan-1.4.0/cli/Harpocrates/detectors/regex_patterns.py +109 -0
- harpo_scan-1.4.0/cli/Harpocrates/envvars.py +31 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/__init__.py +4 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/__main__.py +70 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/blocks.py +36 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/body.py +298 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/config.py +133 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/encoded.py +164 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/errors.py +96 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/finder.py +124 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/headers.py +48 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/jsontext.py +393 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/media.py +72 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/relay.py +106 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/request_log.py +112 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/request_scan.py +223 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/routes.py +55 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/scan.py +96 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/server.py +466 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/session_token.py +102 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/spancache.py +139 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/upstream.py +353 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/walk_anthropic.py +314 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/walk_common.py +194 -0
- harpo_scan-1.4.0/cli/Harpocrates/gate/walk_responses.py +245 -0
- harpo_scan-1.4.0/cli/Harpocrates/gatecheck.py +657 -0
- harpo_scan-1.4.0/cli/Harpocrates/grants.py +210 -0
- harpo_scan-1.4.0/cli/Harpocrates/guard.py +708 -0
- harpo_scan-1.4.0/cli/Harpocrates/harness.py +353 -0
- harpo_scan-1.4.0/cli/Harpocrates/hooks.py +1713 -0
- harpo_scan-1.4.0/cli/Harpocrates/install.py +1020 -0
- harpo_scan-1.4.0/cli/Harpocrates/mcp/__init__.py +0 -0
- harpo_scan-1.4.0/cli/Harpocrates/mcp/server.py +227 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/__init__.py +14 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/context.py +392 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/explain.py +330 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/features.py +1887 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/models/model.onnx +0 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/models/model_config.json +168 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/models/onnx_model_hashes.json +5 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/models/xgboost_model.json +1 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/onnx_verifier.py +609 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/tokens.py +25 -0
- harpo_scan-1.4.0/cli/Harpocrates/ml/verifier.py +112 -0
- harpo_scan-1.4.0/cli/Harpocrates/paths.py +378 -0
- harpo_scan-1.4.0/cli/Harpocrates/read.py +495 -0
- harpo_scan-1.4.0/cli/Harpocrates/utils/__init__.py +1 -0
- harpo_scan-1.4.0/cli/Harpocrates/utils/file_utils.py +92 -0
- harpo_scan-1.4.0/cli/Harpocrates/utils/redaction.py +111 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/PKG-INFO +494 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/SOURCES.txt +73 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/dependency_links.txt +1 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/entry_points.txt +5 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/requires.txt +32 -0
- harpo_scan-1.4.0/cli/harpo_scan.egg-info/top_level.txt +1 -0
- harpo_scan-1.4.0/pyproject.toml +177 -0
- harpo_scan-1.4.0/setup.cfg +4 -0
harpo_scan-1.4.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
### File: `LICENSE`
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Joshua Do
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice (including the next
|
|
19
|
+
paragraph) shall be included in all copies or substantial portions of the
|
|
20
|
+
Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harpo-scan
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Secrets scanner with a bundled ML model, plus a redacting read tool for AI coding agents
|
|
5
|
+
Author-email: Joshua Do <joshalfonsodo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Skipa776/Harpocrates
|
|
8
|
+
Project-URL: Repository, https://github.com/Skipa776/Harpocrates
|
|
9
|
+
Project-URL: Issues, https://github.com/Skipa776/Harpocrates/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: typer>=0.9.0
|
|
22
|
+
Requires-Dist: rich>=13.0.0
|
|
23
|
+
Requires-Dist: onnxruntime>=1.17.0
|
|
24
|
+
Requires-Dist: numpy>=1.24.0
|
|
25
|
+
Requires-Dist: zstandard>=0.22; python_version < "3.14"
|
|
26
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: zstandard>=0.22; extra == "dev"
|
|
34
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
36
|
+
Provides-Extra: mcp
|
|
37
|
+
Requires-Dist: mcp<2,>=1.27; extra == "mcp"
|
|
38
|
+
Provides-Extra: explain
|
|
39
|
+
Requires-Dist: xgboost<4,>=2.0.0; extra == "explain"
|
|
40
|
+
Provides-Extra: native
|
|
41
|
+
Requires-Dist: harpo-native==1.4.0; extra == "native"
|
|
42
|
+
Provides-Extra: train
|
|
43
|
+
Requires-Dist: xgboost<4,>=2.0.0; extra == "train"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
<h1 align="center">Harpo</h1>
|
|
49
|
+
|
|
50
|
+
Harpo is a secrets scanner and a guard that keeps secrets out of AI coding agents (Claude Code and Codex). The scanner finds candidates with regex patterns and Shannon entropy, then a bundled ML model decides on the ones no pattern settles, with no network calls. The guard puts two layers around the agent: a read tool and hook that keep secrets out of what the agent reads, and an egress gate that scans every request the agent sends to its model provider.
|
|
51
|
+
|
|
52
|
+
**Status.** Version 1.4.0 (`pip install harpo-scan`) has all of it: the scanner, the read tool and hook, the egress gate, `harpo guard`, `harpo doctor`, the audit logs and the Rust rules. No further features are planned; what I would build next is under [If I kept going](#if-i-kept-going). Requirements, decisions and progress are tracked in [docs/design.md](docs/design.md).
|
|
53
|
+
|
|
54
|
+
## At a glance
|
|
55
|
+
|
|
56
|
+
| Layer | What it does | Status | Evidence |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| Scanner (`harpo scan`, pre-commit) | Regex and Shannon entropy (in Rust when the optional extension or binary is built, else the same rules in Python), then a bundled XGBoost model (ONNX, frozen at v17) decides. No network calls. | Released (v1.4.0) | 95.0% recall and 90.4% precision on a 4,352-record held-out benchmark, at the recall-first threshold. That set is half secrets; on real code most alarms are false (see [Precision on real code](#precision-on-real-code)). |
|
|
59
|
+
| Read tool (`safe_read`, `safe_grep`) and read hook | The agent's reads of secret-bearing files are refused. The agent reads them through an MCP tool that returns placeholders. | Released | Scripted sessions of the real Claude Code and Codex CLIs against a local mock provider: 0 of 22 known-format canaries received; 10 of 22 without Harpo. |
|
|
60
|
+
| Egress gate | A localhost proxy that parses every request to the model provider, replaces secrets with placeholders, and blocks what it cannot rewrite safely. Fails closed. | Released (v1.4.0) | 0 of 57 known-format canaries received in a corpus of real request shapes, on two seeds; 0 received in every gated agent session. |
|
|
61
|
+
| `harpo guard up` / `down`, `harpo doctor` | Routes both agents through the gate from their own settings files, and restores them byte-identical. | Released (v1.4.0) | Both agents routed by `guard up` alone: 0 of 11 known-format received, and `harpo doctor` passed. After `guard down`, the files are byte-identical and the same reads leak 5 of 11. |
|
|
62
|
+
| Audit logs (`harpo proof`) | A findings log from every layer, and a hash-chained proof log of what the gate forwarded. Hashes and counts only. | Released (v1.4.0) | Every forwarded request has a proof entry whose SHA-256 matches what the mock received. A sweep of every Harpo file found 0 canary values. |
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
flowchart LR
|
|
68
|
+
agent["Coding agent"]
|
|
69
|
+
readtool["Read tool and hook"]
|
|
70
|
+
gate["Egress gate on 127.0.0.1"]
|
|
71
|
+
provider["Model provider"]
|
|
72
|
+
core["Detection core: regex, entropy, ONNX model"]
|
|
73
|
+
scan["harpo scan and pre-commit"]
|
|
74
|
+
findings["Findings log"]
|
|
75
|
+
proof["Proof log"]
|
|
76
|
+
|
|
77
|
+
agent -- "file reads" --> readtool
|
|
78
|
+
readtool --> agent
|
|
79
|
+
agent -- "every API request" --> gate
|
|
80
|
+
gate -- "redacted request" --> provider
|
|
81
|
+
provider -- "response" --> gate
|
|
82
|
+
gate --> agent
|
|
83
|
+
readtool --> core
|
|
84
|
+
gate --> core
|
|
85
|
+
scan --> core
|
|
86
|
+
readtool --> findings
|
|
87
|
+
gate --> findings
|
|
88
|
+
gate --> proof
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **Why two layers.** The read tool sees file context, but the agent can still `cat` a file through the shell. The gate sees every request, whatever tool produced it (complete mediation).
|
|
92
|
+
- **No TLS interception.** The agent sends plain HTTP to 127.0.0.1, and the gate opens its own TLS connection upstream.
|
|
93
|
+
- **Fail closed.** A scanner error, a body it cannot parse, an unknown content encoding or an audit log it cannot write blocks the request with a reason code (HARPO-BLOCK-*). Nothing is forwarded unscanned.
|
|
94
|
+
|
|
95
|
+
## What is proven and what is not
|
|
96
|
+
|
|
97
|
+
**Proven:**
|
|
98
|
+
|
|
99
|
+
- The agent-session results in At a glance, with Claude Code 2.1.306 and codex-cli 0.163.0, on Oct 21, 2026. Every run uses generated fake canaries, throwaway home folders and a local mock provider; no real credential and no real provider is involved. Commands and results are in [docs/testing.md](docs/testing.md) and the design doc's Progress section.
|
|
100
|
+
- Fail closed: when the scanner errors, both agents are shown the block, and 0 requests reach the provider.
|
|
101
|
+
- The gate adds 12.2 ms p95 on follow-up turns and 347 ms p95 on first turns with a cold cache, over sessions that include 0.7 to 1 MB requests (measured locally on an Apple M3 Pro, Oct 20 to 21). CI now runs the same check, `bench/check_latency.py`, on agent sessions it records, and reports without blocking. Replaying 20 recorded sessions of about 45 KB per request on a 4-core Linux container (Oct 28), the gate adds 46.5 ms p95 on first turns with the Rust rules in process and 65.9 ms with the Python rules, and about 11 ms on follow-ups either way.
|
|
102
|
+
- 3,455 unit tests on both scan engines and on Python 3.10, 3.11 and 3.14, 93.8% line coverage (floor 90% in CI), 13 integration tests (three install the built wheel in a fresh venv and run it in a throwaway home; the agent suites drive the real Claude Code and Codex CLIs against the mock), and 23 Rust tests. CI runs all of them, the unit suite on macOS and the plugin tests on every push to a pull request.
|
|
103
|
+
|
|
104
|
+
**Not proven or not done:**
|
|
105
|
+
|
|
106
|
+
- No session against a real provider has run through the gate yet. Streaming and subscription logins are tested against the mock only.
|
|
107
|
+
- The agent sessions are scripted: the mock provider tells each agent which 3 tools to call. They show the protection works inside the real CLIs, not how often a model would find a way around it. In three live Claude Code runs (Oct 19), none of 15 canaries reached the agent's context; that is too few to estimate a leak rate.
|
|
108
|
+
- Secrets with no known format (the model's job) do get through the gate: 12 to 13 of 23 such canaries in the request corpus.
|
|
109
|
+
- The read hook covers a fixed list of shell read commands. `python -c "print(open('.env').read())"` gets past it, and the gate then sees the request.
|
|
110
|
+
- Traffic the agent sends anywhere other than its model API (for example, `curl` to another server) is out of scope.
|
|
111
|
+
- The Rust rules are optional. `pip install "harpo-scan[native]"` adds the in-process module that the read tool, the hook, the gate and the scanner use, prebuilt for Linux and macOS (x86_64 and arm64); the scanner binary needs `cargo build` from a clone. Without either, the same rules run in Python. Both give identical candidates on this repository and on a fixture with a line for every rule; outside ASCII, a few character tests (what counts as a digit or a letter) are Rust's nearest equivalents of Python's and could differ on unusual characters.
|
|
112
|
+
- The model is frozen at v17. ML work stopped on Oct 28, 2026, so its known misses (below) will not be retrained away, and the planned review of the real-code negatives will not happen.
|
|
113
|
+
- Task success with the guard on, an evasion suite, and setup time measured by someone else are planned (M3), not done.
|
|
114
|
+
|
|
115
|
+
## Demos
|
|
116
|
+
|
|
117
|
+
Both demos run on generated canary secrets, never real ones, in a throwaway home directory. The tapes are in [docs/tapes/](docs/tapes/) and CI re-renders each GIF from its tape.
|
|
118
|
+
|
|
119
|
+
### Scene 1: `harpo scan` finds secrets a regex scanner misses
|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
Measured on the seed-0 canary repo (15 planted secrets in 8 files, made by `bench/canary_repo.py`), with `harpo` from `main` before the 1.4.0 version bump at its default scan settings and gitleaks 8.30.1 at its default rules: harpo found 14 of 15 canaries and gitleaks found 11 of 15. Only harpo found the `DATABASE_URL` connection string; that is two of the canaries, the URL and the password inside it, because one finding covers both. Harpo also flagged the AWS access key id that gitleaks did not, but that one is not evidence: the generator draws the id from `A-Z0-9`, gitleaks's AWS rule accepts only the base32 characters `A-Z2-7` that real ids use, and gitleaks does find a base32-valid key. Gitleaks found the JWT, and harpo flagged that line through its entropy check. Neither found `POSTGRES_PASSWORD: ...` in the compose file. Per-secret results: [bench/results/demos/scan-vs-gitleaks.json](bench/results/demos/scan-vs-gitleaks.json); reproduce with `python bench/demos_measure.py`.
|
|
124
|
+
|
|
125
|
+
Transcript (secrets shown redacted, as the tools print them):
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
$ gitleaks dir .env --no-banner --redact -v 2>&1 | grep -E 'RuleID|Line:|leaks found'
|
|
129
|
+
RuleID: generic-api-key
|
|
130
|
+
Line: 2
|
|
131
|
+
RuleID: generic-api-key
|
|
132
|
+
Line: 3
|
|
133
|
+
WRN leaks found: 2
|
|
134
|
+
$ harpo scan .env
|
|
135
|
+
Found 5 potential secrets
|
|
136
|
+
CRITICAL AWS_ACCESS_KEY_ID .env:1 AKIA...ISIG
|
|
137
|
+
CRITICAL OPENAI_API_KEY .env:3 sk-E...svk9
|
|
138
|
+
HIGH ENV_ASSIGNMENT .env:4 post...vice
|
|
139
|
+
INFO ENTROPY_CANDIDATE .env:2 AWS_...LpQA
|
|
140
|
+
MEDIUM ENV_ASSIGNMENT .env:2 GsNm...LpQA
|
|
141
|
+
$ # whole repo: harpo found 14 of 15 canaries, gitleaks 11 of 15
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Scene 2: an agent's read is blocked, and `safe_read` returns placeholders
|
|
145
|
+
|
|
146
|
+

|
|
147
|
+
|
|
148
|
+
This scene is scripted, not a live session: a Claude Code `PreToolUse` event for reading `.env` is piped into the hook, then the MCP server's `safe_read` function is called directly. The placeholder hashes change on every run.
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
$ python -m Harpocrates.hooks < event.json
|
|
152
|
+
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": ".env (api_token, cloud_key, db_credential) contains secrets. Use mcp__harpo__safe_read (...) instead: it returns the same content with each secret replaced by a <<HARPO:type:hash>> placeholder."}}
|
|
153
|
+
$ python -c "from Harpocrates.mcp.server import safe_read; print(safe_read('.env'))"
|
|
154
|
+
1 AWS_ACCESS_KEY_ID=<<HARPO:cloud_key:7fd7>>
|
|
155
|
+
2 AWS_SECRET_ACCESS_KEY=<<HARPO:api_token:ff3f>>
|
|
156
|
+
3 OPENAI_API_KEY=<<HARPO:api_token:9e29>>
|
|
157
|
+
4 DATABASE_URL=postgresql:<<HARPO:db_credential:d151>>
|
|
158
|
+
5 DEBUG=true
|
|
159
|
+
[harpo: 4 secret(s) redacted as <<HARPO:type:hash>>. They are not the real values; reference the variable or config key instead of copying them.]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Challenges and what went wrong
|
|
163
|
+
|
|
164
|
+
These are the problems I ran into, what I changed for each, and what each one taught me.
|
|
165
|
+
|
|
166
|
+
- **Entropy and the model flagged every random-looking string, including images in web pages.** Early versions turned any long high-entropy token into a candidate, so inline base64 images (`data:` URIs) in HTML and CSS, webpack content hashes, CSS class fingerprints and SVG path data were all flagged, and the ML step did not filter them out. In v0.2.0 (Apr 2026) I stripped URLs and `data:` URIs before tokenizing and added filters for camelCase and lowercase-slug tokens, which cut false positives on a real codebase from 1,299 to 16; a May 2026 fix then skips the entropy step for .html, .css, .svg and .xml files while keeping the regex signatures, so an API key pasted into a `<script>` block is still caught. Lesson: entropy measures randomness, not secrecy, and the kind of file matters.
|
|
167
|
+
- **The model learned a shortcut from variable names.** With variable-name features, the model leaned on names like `password` instead of the value. Zeroing the 10 variable-name features in training raised validation AUC, so the shipped model judges the value and its surroundings, never the name. Lesson: a feature that makes the training data easy can make the real task harder.
|
|
168
|
+
- **I reused my benchmark until it stopped being a test.** The held-out benchmark was scored for several model versions, so it was no longer a clean test for choosing the next one. I retired it for model selection and kept it only as the published measure of the shipped model. Lesson: a held-out set is only held out the first time you look at it.
|
|
169
|
+
- **A large file was scanned only partly, and the scan still passed.** A single-file scan read only the first 10 MB of a larger file without saying so, so a secret after that point was missed with a clean exit. Now every file is scanned whole, in bounded pieces under a 64 MB memory cap, and a test plants a secret on the last line of a 100 MB file. Lesson: a size limit in a security tool must fail loudly, never quietly pass.
|
|
170
|
+
- **The Rust engine drifted from the Python one.** The Rust scanner had older copies of some patterns and lacked a whole rule phase, so on this repository it found 140 of the Python engine's 159 findings, while its parity test passed because it only compared seven sample files that none of the missing rules touched. Now Rust reports every pattern by name and a test compares the two lists, and both engines are compared field by field on a fixture with a line for every rule. Lesson: a parity test is only as good as the cases it covers.
|
|
171
|
+
- **Blocking reads in the agent was not enough.** The hook that refuses an agent's reads of secret-bearing files covers a fixed list of shell commands, and `python -c "print(open('app.yaml').read())"`, `perl`, `vim -es` and `xargs cat` all got through in testing. That is why I built the egress gate, which scans what actually leaves the machine, whatever tool produced it. Lesson: guard the boundary the data crosses, not each way of reaching it.
|
|
172
|
+
|
|
173
|
+
## If I kept going
|
|
174
|
+
|
|
175
|
+
Development has stopped, and these are the next steps I would take.
|
|
176
|
+
|
|
177
|
+
- **Learn from the user's "not a secret" answers, locally.** When Harpo flags a value the user knows is harmless, it should remember the answer so the same value is not flagged on the next run. The design constraints are already worked out: the hook cannot show a dialog inside the agent and has a time budget of about 25 seconds, so the read is refused at once and the question is asked out of band. The answer is stored as a keyed fingerprint (a hash, never the value), and only the user can answer, never the agent, so a prompt-injected agent cannot teach Harpo to ignore a real secret. A small local model could later learn the places where false alarms cluster. The goal is to learn your false alarms, not your secrets.
|
|
178
|
+
- **Protect more than credentials: high-risk personal data.** The egress gate already parses every request an agent sends and rewrites matching strings to placeholders, so the same machinery could keep the highest-risk class of data (P4 in a P1 to P4 data classification: Social Security numbers, health records, payment card and bank details) from reaching a model provider. This would likely matter to more people than API keys. The hard part is false alarms: a nine-digit number is usually not a Social Security number, so it would need validity checks (for example the Luhn check for card numbers) and context, and it was out of scope for this version.
|
|
179
|
+
|
|
180
|
+
## Model card
|
|
181
|
+
|
|
182
|
+
- **Model:** v17 (detector v1.2), frozen: no further training is planned.
|
|
183
|
+
- **Architecture:** XGBoost (600 trees, depth 7), inference via ONNX Runtime. The input vector has 64 features, but the trees split on 53 of them. The 10 variable-name features are zeroed in training because they were a shortcut: removing them raised validation AUC. The model therefore judges the value and its surroundings, never the name (`pass`, `greeting` and `x` score alike). One more feature, `value_is_dotted_quad_or_host`, is never chosen.
|
|
184
|
+
- **Distribution:** bundled in the package — no training needed, no network calls. Harpo turns off ONNX Runtime's built-in Microsoft telemetry, which is on by default since ONNX Runtime 1.31. If you import onnxruntime yourself before Harpo in the same process, export ORT_DISABLE_TELEMETRY=1.
|
|
185
|
+
- **Training data:** synthetic + LLM-written code + real open-source negatives (157,066 candidate rows)
|
|
186
|
+
- **Thresholds:** two operating points from one model:
|
|
187
|
+
- commit `0.5559` — precision-first, used by `harpo scan`
|
|
188
|
+
- gate `0.1904` — recall-first, used by the read tool
|
|
189
|
+
|
|
190
|
+
| Operating point | Benchmark recall | Benchmark precision | OSS recall | OSS precision | AUC (benchmark / OSS) |
|
|
191
|
+
|---|---|---|---|---|---|
|
|
192
|
+
| Gate | 95.0% | 90.4% | 99.3% | 94.5% | 0.986 / 1.000 |
|
|
193
|
+
| Commit | 93.9% | 94.4% | 98.7% | 97.1% | same model |
|
|
194
|
+
|
|
195
|
+
Both test sets are about half secrets, so these precision figures are far higher than on real code. See [Precision on real code](#precision-on-real-code).
|
|
196
|
+
|
|
197
|
+
Reported confidence is calibrated; per-type recall and precision are in [docs/detector-report.md](docs/detector-report.md).
|
|
198
|
+
|
|
199
|
+
Known limits:
|
|
200
|
+
|
|
201
|
+
- The benchmark is synthetic; real repositories will differ.
|
|
202
|
+
- A 64-character hex HMAC signing key (`[]byte("7d4e9a1f…")` in Go) is missed.
|
|
203
|
+
- Passwords in test files are missed. `pass = "Summer2024!"` in `test_x.py` scores 0.008, below even the gate threshold; the same line in `app.yaml` scores 0.63. Being in a test file is the second-strongest signal in the model and outweighs the shape of the value.
|
|
204
|
+
- Human-style passwords such as `Summer2024!` only just clear the commit threshold (calibrated confidence 0.56).
|
|
205
|
+
- Regex hits skip the model. Documentation-style example keys that match a provider regex are always flagged. This sets a floor on the false-positive rate: 2.8% on the benchmark and 1.6% on open-source code, whatever the threshold.
|
|
206
|
+
- Recall ceiling (the share of secrets the scanner turns into a candidate at all): 95.5% benchmark / 99.5% OSS.
|
|
207
|
+
|
|
208
|
+
Evaluation data was never used for training. The benchmark has been scored for several model versions, so it is no longer a clean test for choosing a model; since no new model is planned, it stays as the published measure of v17.
|
|
209
|
+
|
|
210
|
+
## Install
|
|
211
|
+
|
|
212
|
+
**Names.** Version 1.4.0 renamed the tool to Harpo: the command is `harpo` and the PyPI package is `harpo-scan`. The old names still work for two more minor releases; see the rename note near the end.
|
|
213
|
+
|
|
214
|
+
### 1. Scanner CLI
|
|
215
|
+
|
|
216
|
+
Install the scanner and run it on a folder. The ML model is included.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pip install harpo-scan # the scanner CLI (ML model included)
|
|
220
|
+
harpo scan .
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`--version` prints the installed version.
|
|
224
|
+
|
|
225
|
+
### 2. Protect your AI agent (Claude Code, Codex)
|
|
226
|
+
|
|
227
|
+
This adds the read tool and the hooks that keep the agent off secrets. Install the CLI (it includes the read tool and the hooks):
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pip install "harpo-scan[mcp]"
|
|
231
|
+
harpo install # every detected harness: Claude Code and/or Codex
|
|
232
|
+
harpo install --claude-code # or name one: --codex
|
|
233
|
+
harpo install --dry-run # show what would change; write and run nothing
|
|
234
|
+
harpo uninstall # removes exactly what `install` added; backups stay
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The first install takes a few minutes: it downloads ONNX Runtime and NumPy for the bundled model.
|
|
238
|
+
|
|
239
|
+
`harpo install` sets up three things: deny rules for files named like secrets, the MCP registration, and a content-aware hook that refuses built-in and shell reads of any file holding a secret and points the agent to `safe_read`.
|
|
240
|
+
|
|
241
|
+
Codex needs one manual step: start `codex`, type `/hooks`, and trust the Harpo hook once. `harpo setup` lists detected harnesses and prints the manual steps for each instead of writing them.
|
|
242
|
+
|
|
243
|
+
Setup steps are in [docs/setup.md](docs/setup.md). What the setup does and doesn't protect is in [docs/threat-model.md](docs/threat-model.md).
|
|
244
|
+
|
|
245
|
+
### 3. Egress gate
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pip install "harpo-scan[mcp]"
|
|
249
|
+
harpo guard up # start the gate on 127.0.0.1 with a new token; route Claude Code and Codex to it
|
|
250
|
+
harpo doctor # check locally that each agent is routed to the running gate and nothing bypasses it
|
|
251
|
+
harpo proof # the last forwarded requests, as hashes and placeholder counts; verifies the chain
|
|
252
|
+
harpo guard down # restore both agents' settings files byte for byte and stop the gate
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Restart running agent sessions after `guard up`. While the agents are routed, a stopped gate makes their requests fail to connect; nothing is sent unscanned. Details are in [docs/setup.md](docs/setup.md#5-egress-gate-harpo-guard-up).
|
|
256
|
+
|
|
257
|
+
### 4. Claude Code plugin (optional)
|
|
258
|
+
|
|
259
|
+
In Claude Code, run:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
/plugin marketplace add Skipa776/harpocrates
|
|
263
|
+
/plugin install harpocrates-prompt --marketplace Skipa776/harpocrates
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The plugin shows the "edit without secret protection" question as a styled card and adds `/harpo` allowlist commands to the chat box. It needs the CLI hook from step 2.
|
|
267
|
+
|
|
268
|
+
### 5. Pre-commit hook (optional)
|
|
269
|
+
|
|
270
|
+
Add to `.pre-commit-config.yaml`, then run `pre-commit install`:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
repos:
|
|
274
|
+
- repo: https://github.com/Skipa776/Harpocrates
|
|
275
|
+
rev: v1.4.0
|
|
276
|
+
hooks:
|
|
277
|
+
- id: harpo
|
|
278
|
+
args: ["--ignore", "graphify-out,*.lock"] # optional
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## How agent protection works
|
|
282
|
+
|
|
283
|
+
With step 2 in place, the agent is kept off secrets like this:
|
|
284
|
+
|
|
285
|
+
- A read of a file that holds a secret is refused, with no prompt. The agent uses `safe_read` or `safe_grep` instead, and the secrets come back as placeholders.
|
|
286
|
+
- Editing such a file asks once per file and session. Yes allows built-in Read and Edit of that file until the session ends.
|
|
287
|
+
- An edit that would write a placeholder is refused.
|
|
288
|
+
- Codex is refuse-only: its hooks can't ask, so a Codex edit to such a file is refused and you make that edit yourself.
|
|
289
|
+
|
|
290
|
+
The gate is the layer that sees what the hook misses, such as a program that opens a file itself. What has and has not been shown for each layer is under [What is proven and what is not](#what-is-proven-and-what-is-not).
|
|
291
|
+
|
|
292
|
+
## Allowlist
|
|
293
|
+
|
|
294
|
+
A file or folder that holds secrets but that you want the agent to read can be allowed. In a terminal:
|
|
295
|
+
|
|
296
|
+
- `harpo allow PATH` allows a file or folder. A folder covers everything under it.
|
|
297
|
+
- `harpo allow --name NAME` allows a folder or file name in every project, such as `graphify-out`.
|
|
298
|
+
- `--remove` takes one off the list and `--list` shows it.
|
|
299
|
+
|
|
300
|
+
With the Claude Code plugin, the same works from the chat box: `/harpo allow <path>`, `/harpo allow --name <name>`, `/harpo remove …` and `/harpo allowlist`. These run only when you type them there.
|
|
301
|
+
|
|
302
|
+
Only you can change the allowlist. The agent's own commands that call `harpo allow` or touch `~/.harpo` or `~/.harpocrates` are refused. Names that look like secret files, credential folders, `.harpo`, `.harpocrates`, wildcards and path separators are refused too. Every use of an allowlisted path is logged to `~/.harpo/allow.log`. See [docs/setup.md](docs/setup.md#allowlisting-paths-and-names).
|
|
303
|
+
|
|
304
|
+
## Using the scanner
|
|
305
|
+
|
|
306
|
+
Scan a folder:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
harpo scan ./my_project # the ML model always runs
|
|
310
|
+
harpo scan ./my_project --json # pipe-friendly JSON output
|
|
311
|
+
harpo scan ./my_project --fail-on high
|
|
312
|
+
harpo scan ./my_project --history
|
|
313
|
+
harpo scan ./my_project --engine rust # needs cargo build --release --manifest-path core/Cargo.toml
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`--engine rust` changes only where the regex and entropy rules run; the ML step is the same Python code. In auto mode, `harpo scan` uses the Rust binary when one is built in the checkout or on the PATH; otherwise it scans in process, in Rust when `harpo_native` is installed (`pip install "harpo-scan[native]"`, or `pip install ./core` from a clone) and in Python when not. The Rust core has every rule of the Python detector, with the same pattern text (`harpocrates-rust-scanner patterns` lists them, and a test compares the two lists). On this repository (310 files, a 4-core Linux container, Oct 28) both engines report the same 2,236 raw candidates, field by field, and the same 159 findings after the model. Without the model, Rust takes 0.8 s and Python 5.0 s; with it, 1.9 s and 6.5 s, since the model runs in Python either way.
|
|
317
|
+
|
|
318
|
+
Exclude files and directories with `--ignore`, a comma-separated list:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
harpo scan . --ignore "graphify-out,tests" # directories by name
|
|
322
|
+
harpo scan . --ignore "*.json,*.min.js" # files by glob
|
|
323
|
+
harpo scan . --ignore "graphify-out,*.lock,fixtures" # both
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- A pattern without a wildcard matches a file or directory name exactly, at any depth. `graphify-out` skips that directory wherever it appears.
|
|
327
|
+
- A pattern with `*`, `?` or `[` is a glob matched against each name in the path.
|
|
328
|
+
- Patterns can't contain `/`: `app/static` matches nothing, so use `static`, which skips every directory with that name.
|
|
329
|
+
- Skipped by default: `.git`, `node_modules`, virtual environments, `dist`, `build`, `target`, caches, IDE folders, compiled files, archives and images. Symlinks are never followed.
|
|
330
|
+
- There is no ignore file yet, and `.gitignore` isn't read.
|
|
331
|
+
|
|
332
|
+
## MCP tools and placeholders
|
|
333
|
+
|
|
334
|
+
The MCP server listens on stdio (`harpo-mcp`, requires Python 3.10+) and exposes four tools:
|
|
335
|
+
|
|
336
|
+
- `scan_text` / `scan_file` — scan a string or a file, return findings.
|
|
337
|
+
- `safe_read` — file contents with every detected secret replaced by a placeholder such as `<<HARPO:api_token:3f9a>>`.
|
|
338
|
+
- `safe_grep` — search results produced after redaction, so a search can't reveal a secret.
|
|
339
|
+
|
|
340
|
+
Placeholder design:
|
|
341
|
+
|
|
342
|
+
- The tag is an HMAC of the value under a random 32-byte key that exists only in memory for the session. A plain hash would let anyone who sees the placeholder test guesses offline, which is cheap for a short password, and would give a secret the same tag across sessions.
|
|
343
|
+
- The same secret gets the same placeholder throughout a session, so the agent can tell repeated uses apart from different values. Values are never restored.
|
|
344
|
+
- The tag is 4 hex characters (16 bits). That keeps it short and reveals little about the value, but two different secrets of the same type can collide: about 1% likely with 36 such secrets in a session, 50% with 300. A collision makes the agent think two secrets are the same. Since values are never restored, it can't leak or misplace a secret.
|
|
345
|
+
|
|
346
|
+
Failure handling: if the model can't load, for example because its hash doesn't match, `safe_read` and `safe_grep` redact every candidate, the hook refuses files that contain any candidate, and `harpo scan` exits 2. There is no fallback to an older model.
|
|
347
|
+
|
|
348
|
+
## Benchmarks
|
|
349
|
+
|
|
350
|
+
Measured at v1.3.0 with model v17 and not re-run since. Each later change to the detector was checked to leave its findings on this repository's files unchanged; the one intended change, scanning files over 10 MB whole (FR-CORE-05), does not touch these short records. Five scanners, same files, full scans. Two test sets, never used for training or tuning: a 4,352-record benchmark written by LLMs that generated none of the training data (half the records carry a secret, half a harmless look-alike), and 3,000 records from 42 permissively licensed GitHub repos with fake secrets inserted and real high-entropy strings as negatives. Recall and precision columns are benchmark first, then open-source code.
|
|
351
|
+
|
|
352
|
+
| Scanner | Recall | Precision | False-positive rate | Recall | Precision | AUC (benchmark / OSS) |
|
|
353
|
+
|---|---|---|---|---|---|---|
|
|
354
|
+
| TruffleHog 3.95 | 40.0% | 97.6% | 0.9% | 39.5% | 98.8% | n/a (yes/no only) |
|
|
355
|
+
| gitleaks 8.30 | 52.3% | 89.3% | 5.7% | 48.9% | 100.0% | n/a (yes/no only) |
|
|
356
|
+
| detect-secrets 1.5 ¹ | 55.3% | 63.8% | 28.3% | 74.0% | 78.7% | n/a (yes/no only) |
|
|
357
|
+
| CredSweeper 1.18 | 68.3% | 72.8% | 23.1% | 59.3% | 93.3% | 0.72 / 0.81 |
|
|
358
|
+
| Harpo (gate) | 95.0% | 90.4% | 9.1% | 99.3% | 94.5% | 0.986 / 1.000 ² |
|
|
359
|
+
| Harpo (commit) | 93.9% | 94.4% | 5.1% | 98.7% | 97.1% | same model |
|
|
360
|
+
|
|
361
|
+
¹ detect-secrets reports only a hash of each secret, so it is credited for any finding on the right line. That is lenient in its favor.
|
|
362
|
+
² Harpo AUC is measured over the candidates the scanner extracts. TruffleHog, gitleaks and detect-secrets output yes/no only, so they have one point on the ROC curve, not a curve. CredSweeper's AUC uses its ML probabilities (`--ml_threshold 0`).
|
|
363
|
+
|
|
364
|
+
**Compare recall only at matched false-positive rates.** The rows above run at different false-positive rates. The gate's 95.0% comes with 9.1% false positives, ten times TruffleHog's 0.9%. TruffleHog, gitleaks and detect-secrets have no threshold, so the fair question is what Harpo's recall is at *their* false-positive rate. Benchmark results for the shipped model at several thresholds:
|
|
365
|
+
|
|
366
|
+
| Harpo threshold | Recall | False-positive rate | Precision |
|
|
367
|
+
|---|---|---|---|
|
|
368
|
+
| 0.1904 (gate) | 95.0% | 9.1% | 90.4% |
|
|
369
|
+
| 0.5559 (commit) | 93.9% | 5.1% | 94.4% |
|
|
370
|
+
| 0.80 | 92.2% | 3.8% | 95.7% |
|
|
371
|
+
| 0.95 | 84.3% | 3.3% | 95.8% |
|
|
372
|
+
| regex hits only | 43.8% | 2.8% | 93.3% |
|
|
373
|
+
|
|
374
|
+
- **Against gitleaks (5.7% false positives):** the commit threshold is at 5.1%, with 93.9% recall against gitleaks' 52.3%.
|
|
375
|
+
- **Against TruffleHog (0.9%):** Harpo can't get that low. Regex hits bypass the model, and 58 of the 65 regex false positives are documentation example keys. That floors the rate at 2.8%, where recall is 43.8%, about TruffleHog's level at three times its false-positive rate.
|
|
376
|
+
- **The model itself is not the limit:** counting only the model's false positives, it reaches 91.9% recall at 0.9%. Sending regex hits on example keys through the model would remove the floor; with ML work stopped, that is not planned.
|
|
377
|
+
|
|
378
|
+
### Precision on real code
|
|
379
|
+
|
|
380
|
+
Both test sets are about half secrets, and real code is nowhere near that. Precision falls with the share of secrets, even when recall and the false-positive rate stay the same. Applying the benchmark's recall and false-positive rates at lower base rates gives:
|
|
381
|
+
|
|
382
|
+
| Share of scanned strings that are secrets | Harpo gate | Harpo commit | TruffleHog |
|
|
383
|
+
|---|---|---|---|
|
|
384
|
+
| 47% (benchmark) | 90% | 94% | 98% |
|
|
385
|
+
| 1% | 9.5% | 16% | 31% |
|
|
386
|
+
| 0.1% | 1.0% | 1.8% | 4.3% |
|
|
387
|
+
|
|
388
|
+
The benchmark's negatives are hard look-alikes, not ordinary code, so treat this table as a rough guide for every tool. The direct measurement is the real-code alarm rate: ML alarms per 1,000 unmodified files from the 16 validation repositories, 56.8 at the gate threshold and 14.9 at commit, with regex alarms on top. At one real secret per 1,000 files, that puts gate precision near 2% and commit precision near 6%. No other scanner has been run on those files yet, so there is no real-code comparison.
|
|
389
|
+
|
|
390
|
+
**Two operating points, one model.** The gate threshold favors recall. It is meant for blocking secrets before they reach an AI model, where a miss is the costly mistake and a false alarm costs one redacted string. The commit threshold is for pre-commit hooks, where false alarms interrupt developers. As the table shows, most of its alarms on real code will still be false.
|
|
391
|
+
|
|
392
|
+
**Secrets only one scanner caught** (held-out benchmark): Harpo **202**, detect-secrets 12, CredSweeper 10, TruffleHog 3, gitleaks 0. Per-type recall and precision are in [docs/detector-report.md](docs/detector-report.md).
|
|
393
|
+
|
|
394
|
+
### Real-world cases none of the others catch
|
|
395
|
+
|
|
396
|
+
Each case below was scanned by all five tools; only Harpo flagged the secret. Values are fake and shortened here; the exact files are in `docs/samples/readme_examples.jsonl`.
|
|
397
|
+
|
|
398
|
+
| Situation | Code | TruffleHog | gitleaks | detect-secrets | CredSweeper | Harpo |
|
|
399
|
+
|---|---|---|---|---|---|---|
|
|
400
|
+
| Database password in a YAML config | `pass: "Summer2024!…"` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
401
|
+
| Password passed as a keyword argument | `psycopg2.connect(…, password="hunter…")` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
402
|
+
| Postgres URL with a percent-encoded password | `postgresql://orders_app:Wint3r%23…@db.internal/orders` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
403
|
+
| Twilio auth token passed positionally | `twilio('AC4f2a…', '9f2c4e1a…')` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
404
|
+
| Random session secret in a Rails config | `config.session_value = "vQ3#kL9$…"` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
405
|
+
| Telegram bot token inside an API URL | `…api.telegram.org/bot7318849210:AAHx9…/getMe` | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
406
|
+
|
|
407
|
+
Why these slip past the others: none has a provider prefix like `AKIA`, `ghp_` or `sk_live_`; several use neutral names (`pass`, `session_value`, a positional argument); two are embedded in a URL. Harpo extracts candidates from connection strings, URL parameters and password-shaped literals, and the model decides which are secrets. Some of these catches are narrow: the `Summer2024!` line only just clears the commit threshold, and the same line in a test file is missed (see Known limits).
|
|
408
|
+
|
|
409
|
+
### Where the others win
|
|
410
|
+
|
|
411
|
+
- **Precision at the extreme:** TruffleHog flags almost nothing that isn't a secret (97.6%, 0.9% false positives). Harpo can't operate below a 2.8% false-positive rate, and on real code TruffleHog's precision is likely higher than either Harpo threshold (see above). TruffleHog also verifies credentials against provider APIs; Harpo never makes network calls.
|
|
412
|
+
- **Specific formats:** CredSweeper catches more JWTs (98.7% vs 94.7%) and Vault tokens (98.6% vs 93.2%); gitleaks and CredSweeper catch every GitHub fine-grained token (100% vs 94.3%).
|
|
413
|
+
- **Connection strings:** TruffleHog catches every JDBC URL (100% vs 96.8%) and more Azure storage connection strings (98.8% vs 97.6%).
|
|
414
|
+
|
|
415
|
+
Recommended setup: Harpo as the pre-commit hook and AI-agent gate (widest coverage), TruffleHog in CI (confirms which leaked credentials are live).
|
|
416
|
+
|
|
417
|
+
How these numbers were produced, and their known biases, is in [Methods](#methods). Reproduce the real-world cases with `bench/readme_examples.py`.
|
|
418
|
+
|
|
419
|
+
## Methods
|
|
420
|
+
|
|
421
|
+
### Test sets
|
|
422
|
+
|
|
423
|
+
Two held-out sets. Neither was used for training, and neither was used to choose the model or set thresholds.
|
|
424
|
+
|
|
425
|
+
**Open-source code (3,000 records).** No one hand-labeled real secrets, and no repository with known real secrets was used. The set is built by planting generated fake secrets into real code (reproduce at tag v1.3.0: `fetch_oss_corpus.py`, `build_eval_set.py`):
|
|
426
|
+
|
|
427
|
+
1. **Corpus.** 200 permissively licensed GitHub repositories (MIT, Apache-2.0, BSD-3-Clause), 20 per language, 500 to 20,000 stars, each pinned to a commit in `oss_repos.tsv` (at tag v1.3.0). Repositories are split into train, validation and test by a hash of their name, so the 42 test repositories never appear in training.
|
|
428
|
+
2. **Positives (1,500).** Fake values from the project's generators, in 24 provider formats plus passwords and connection strings. Each is inserted as a new line at a random position in a real file from a test repository. The variable name is randomly secret-sounding (`api_key`), neutral (`value`), or absent (`client.connect("…")`). The label is known because the value was generated.
|
|
429
|
+
3. **Generated negatives (750).** Look-alikes that are not secrets (git SHAs, UUIDs, checksums, documentation examples, public keys), inserted the same way.
|
|
430
|
+
4. **Real-code negatives (750).** Strings sampled from those that Harpo's own regex and entropy stages extract from the unmodified test repositories. They are labeled not-a-secret by assumption. A string that gitleaks flags, or that matches a Harpo provider regex, is excluded and sent to a review queue instead.
|
|
431
|
+
|
|
432
|
+
**Held-out benchmark (4,352 records).** Code written by two LLMs that generated none of the training data (Kimi K2.6, MiniMax M2.5), with typed placeholders that were afterwards filled with generated fake values or look-alikes. Labels are correct by construction.
|
|
433
|
+
|
|
434
|
+
### Scoring
|
|
435
|
+
|
|
436
|
+
`bench/compare_scanners.py` writes each record as its own file: up to three lines before, the line, and up to three lines after, with the original file extension. All five scanners run on those files.
|
|
437
|
+
|
|
438
|
+
- A scanner gets credit only for a finding on the record's line whose value overlaps the labeled token by at least 4 characters. detect-secrets reports only a hash of each secret, so any finding on the right line counts for it.
|
|
439
|
+
- Recall is the share of positives flagged. Precision is flagged positives divided by all flagged records.
|
|
440
|
+
- TruffleHog runs with `--no-verification`, because every value is fake.
|
|
441
|
+
- Tool versions: TruffleHog 3.95.2, gitleaks 8.30.1, detect-secrets 1.5.0, CredSweeper 1.18.5 at its default threshold.
|
|
442
|
+
|
|
443
|
+
### Known biases and gaps
|
|
444
|
+
|
|
445
|
+
- **The real-code negatives were never reviewed.** The check of at least 500 samples (DATA-05) was not done and, with ML work stopped, will not be, so some "negatives" may be real secrets, and the review queues are unread.
|
|
446
|
+
- **The negatives favor gitleaks.** Real strings gitleaks flagged were removed before scoring, so gitleaks can't produce a false positive on real code. Its 100.0% open-source precision partly reflects this.
|
|
447
|
+
- **The negatives are hard for Harpo.** The real-code negatives are strings Harpo's own extractor picks up, which other scanners may never flag. This penalizes Harpo's precision relative to the others.
|
|
448
|
+
- **The secrets are planted.** Each one is a new line inserted into code that has nothing to do with it. That makes it easier to spot than a secret a developer actually wrote.
|
|
449
|
+
- **Precision is measured on a set that is half secrets.** Real code contains far fewer secrets, so precision on real repositories is much lower; see [Precision on real code](#precision-on-real-code). The real-code alarm rate is the better guide: at the gate threshold, 56.8 alarms per 1,000 files on the 16 validation repositories, where the thresholds were set, and 62.7 on the 42 untouched test repositories.
|
|
450
|
+
- **Recall is compared at different false-positive rates.** The comparison table gives each scanner's own operating point. The matched-rate table above is the fair comparison, and Harpo has no operating point at TruffleHog's 0.9%.
|
|
451
|
+
- **The benchmark is synthetic.** Its code was written by LLMs, not developers, and it has been scored for several model versions, so it is retired for choosing models.
|
|
452
|
+
- **Recall ceiling.** 95.5% of benchmark secrets and 99.5% of open-source secrets become candidates at all; no threshold can recover the rest.
|
|
453
|
+
|
|
454
|
+
These sets are not published yet: the open-source set contains third-party code and is rebuilt from the pinned list instead. Dataset provenance is in [docs/data.md](docs/data.md).
|
|
455
|
+
|
|
456
|
+
## Renamed from Harpocrates
|
|
457
|
+
|
|
458
|
+
v1.3.0 and earlier have only the old names: the commands `harpocrates` and `harpocrates-mcp`, the pre-commit id `harpocrates`, the MCP server `harpocrates` and the folder `~/.harpocrates`. Version 1.4.0 brought the new names below.
|
|
459
|
+
|
|
460
|
+
The project is now called Harpo. Everything you type changes with it:
|
|
461
|
+
|
|
462
|
+
| Before | Now |
|
|
463
|
+
|---|---|
|
|
464
|
+
| `harpocrates`, `harpocrates-mcp` commands | `harpo`, `harpo-mcp`. The old names still work for two more minor releases and print one deprecation line on stderr. |
|
|
465
|
+
| `~/.harpocrates` | `~/.harpo`. The first run copies the old folder there and leaves the original in place; once `~/.harpo` exists, the old folder is no longer read. Environment variables are `HARPO_*`; the `HARPOCRATES_*` names still work. |
|
|
466
|
+
| pre-commit `id: harpocrates` | `id: harpo`. The old id stays as an alias with the same command. |
|
|
467
|
+
| MCP server name `harpocrates` (tools `mcp__harpocrates__safe_read`, `mcp__harpocrates__safe_grep`) | `harpo` (`mcp__harpo__safe_read`, `mcp__harpo__safe_grep`). `safe_read`, `safe_grep` and the `<<HARPO:type:xxxx>>` placeholder are unchanged. |
|
|
468
|
+
| PyPI package `harpocrates` | `harpo-scan`. The old package is now a shim that installs `harpo-scan`. |
|
|
469
|
+
|
|
470
|
+
What to do: install the new package with `pip install harpo-scan`, then re-run `harpo install`. The install moves the MCP server, its allow rules, the notes in `CLAUDE.md` / `AGENTS.md` and the Codex profile over to the new names and takes the old ones out, without dropping any deny rule. `harpo uninstall` removes both the new and the old names. Until you re-run `harpo install`, an install from before the rename keeps working with its old names.
|
|
471
|
+
|
|
472
|
+
## Past versions
|
|
473
|
+
|
|
474
|
+
- **v1.4.0:** the egress gate (Anthropic Messages and OpenAI Responses, including streaming, gzip and zstd bodies, media and base64 inside text); `harpo guard up` / `down`, `harpo doctor` and `harpo proof`; the findings and proof logs; whole-file scans under a 64 MB memory cap; every regex and entropy rule in the Rust core, matching the Python detector, as a binary and as an in-process module (`harpo_native`); the rename to Harpo.
|
|
475
|
+
- **v1.3.0:** agent protection reworked: reads of secret-bearing files are refused (use `safe_read`), edits ask once per file and session; `harpocrates install` / `uninstall` for Claude Code and Codex; allowlist names (`harpocrates allow --name`); Claude Code plugin `harpocrates-prompt` with `/harpocrates` allowlist commands; hook hardening.
|
|
476
|
+
- **v1.2.0** (model v17): higher recall, read tool for AI agents, calibrated confidence. `--ml` was removed so the model always runs (drop `--ml` from scripts and pre-commit `args`; it is now an error); the REST API, LLM verifier, and legacy training CLI were removed.
|
|
477
|
+
- **v0.4:** redesigned 64-feature vector, TreeSHAP `--explain`, severity calibration.
|
|
478
|
+
- **v0.3:** retrained on 62k samples, comment scanning, violation categories.
|
|
479
|
+
- **v0.2:** XGBoost + ONNX model, MCP server, pre-commit hook.
|
|
480
|
+
- **v0.1:** initial regex + entropy pipeline.
|
|
481
|
+
|
|
482
|
+
Full notes for each release are in that tag's README, for example `git show v0.4.0:README.md`.
|
|
483
|
+
|
|
484
|
+
## Contributing
|
|
485
|
+
|
|
486
|
+
False negatives are the highest-priority reports. If Harpo missed a real secret, [open an issue with the `false-negative` label](https://github.com/Skipa776/Harpocrates/issues/new?labels=false-negative).
|
|
487
|
+
For bugs, feature requests, and false positives, open an issue at [github.com/Skipa776/Harpocrates/issues](https://github.com/Skipa776/Harpocrates/issues).
|
|
488
|
+
To run the tests and the end-to-end suites, see [docs/testing.md](docs/testing.md). Common problems are in [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
489
|
+
|
|
490
|
+
Where things are: the Python package (detector, read tool, hook, gate, installer, CLI) is in `cli/Harpocrates/`, with tests named after requirement IDs in `cli/tests/`; the Rust scanner is in `core/`; the mock provider, canary generator and end-to-end suites are in `bench/`; requirements, design decisions and progress are in [docs/design.md](docs/design.md).
|
|
491
|
+
|
|
492
|
+
## License
|
|
493
|
+
|
|
494
|
+
MIT — see [LICENSE](LICENSE).
|