antibody 0.2.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.
- antibody-0.2.0/.github/workflows/publish.yml +49 -0
- antibody-0.2.0/.gitignore +5 -0
- antibody-0.2.0/.python-version +1 -0
- antibody-0.2.0/LICENSE +21 -0
- antibody-0.2.0/PKG-INFO +214 -0
- antibody-0.2.0/README.md +201 -0
- antibody-0.2.0/examples/os-system-formatted-command.py +15 -0
- antibody-0.2.0/examples/os-system-formatted-command.yml +40 -0
- antibody-0.2.0/pyproject.toml +37 -0
- antibody-0.2.0/src/antibody/__init__.py +3 -0
- antibody-0.2.0/src/antibody/__main__.py +3 -0
- antibody-0.2.0/src/antibody/baseline.py +110 -0
- antibody-0.2.0/src/antibody/claude_settings.py +36 -0
- antibody-0.2.0/src/antibody/cli.py +441 -0
- antibody-0.2.0/src/antibody/diffmap.py +46 -0
- antibody-0.2.0/src/antibody/engine.py +141 -0
- antibody-0.2.0/src/antibody/fixtures.py +48 -0
- antibody-0.2.0/src/antibody/gitutil.py +131 -0
- antibody-0.2.0/src/antibody/hook.py +79 -0
- antibody-0.2.0/src/antibody/learn.py +257 -0
- antibody-0.2.0/src/antibody/rules.py +188 -0
- antibody-0.2.0/src/antibody/skill/SKILL.md +72 -0
- antibody-0.2.0/src/antibody/verify.py +197 -0
- antibody-0.2.0/tests/conftest.py +93 -0
- antibody-0.2.0/tests/test_baseline.py +53 -0
- antibody-0.2.0/tests/test_cli.py +100 -0
- antibody-0.2.0/tests/test_diffmap.py +48 -0
- antibody-0.2.0/tests/test_engine.py +22 -0
- antibody-0.2.0/tests/test_fixtures.py +40 -0
- antibody-0.2.0/tests/test_hook.py +77 -0
- antibody-0.2.0/tests/test_learn.py +71 -0
- antibody-0.2.0/tests/test_verify.py +120 -0
- antibody-0.2.0/uv.lock +1816 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a GitHub release is published. It uses trusted publishing, so no
|
|
4
|
+
# token is stored anywhere. One-time setup on PyPI: add a pending publisher with owner
|
|
5
|
+
# "cipheraxat", repository "antibody", workflow "publish.yml" and environment "pypi".
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: false # no shared cache in a release workflow
|
|
25
|
+
- run: uv sync --frozen
|
|
26
|
+
- run: uv run pytest -q
|
|
27
|
+
|
|
28
|
+
pypi:
|
|
29
|
+
needs: test
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment:
|
|
32
|
+
name: pypi
|
|
33
|
+
url: https://pypi.org/p/antibody
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
id-token: write # trusted publishing
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
39
|
+
with:
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
42
|
+
with:
|
|
43
|
+
enable-cache: false
|
|
44
|
+
- name: Check the release tag matches the package version
|
|
45
|
+
run: |
|
|
46
|
+
version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
47
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != v$version"; exit 1; }
|
|
48
|
+
- run: uv build
|
|
49
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
antibody-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cipheraxat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
antibody-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: antibody
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Turn every fix into a verified rule that stops the mistake coming back, enforced on agent edits and in CI.
|
|
5
|
+
Project-URL: Repository, https://github.com/cipheraxat/antibody
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: ruamel-yaml>=0.18
|
|
10
|
+
Provides-Extra: semgrep
|
|
11
|
+
Requires-Dist: semgrep>=1.100; extra == 'semgrep'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# antibody
|
|
15
|
+
|
|
16
|
+
**Every bug you fix, and every mistake you correct your coding agent on, becomes a verified
|
|
17
|
+
rule that stops it coming back.**
|
|
18
|
+
|
|
19
|
+
Agents repeat mistakes. So do teams. A note in `CLAUDE.md` is a suggestion, and the
|
|
20
|
+
lesson from a fix is usually gone the moment it merges. `antibody` keeps it. Your coding
|
|
21
|
+
agent drafts a [Semgrep](https://semgrep.dev)-format rule from the fix. `antibody` *proves*
|
|
22
|
+
the rule fires on the mistake and stays silent on the fix. It then enforces the rule on
|
|
23
|
+
every file the agent edits and in CI, with no LLM, no network and no tokens. Only new
|
|
24
|
+
matches count, so turning it on never breaks a build.
|
|
25
|
+
|
|
26
|
+
The LLM writes the check once. A deterministic tool runs it from then on.
|
|
27
|
+
|
|
28
|
+
## What it looks like
|
|
29
|
+
|
|
30
|
+
A fix made timestamps timezone-aware. Later, Claude Code writes `datetime.utcnow()` in
|
|
31
|
+
another file, and the edit hook answers right away:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
antibody: this edit repeats a mistake that was fixed before
|
|
35
|
+
app/invoices.py:5 [naive-datetime] This makes a timestamp with no timezone, which breaks comparisons and shifts times across servers. Use datetime.now(timezone.utc).
|
|
36
|
+
learned from 95ffe5603abe "Use timezone-aware timestamps in billing"
|
|
37
|
+
Fix the code. Ask the user before suppressing it with `# nosemgrep`.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Claude Code shows that message to Claude, which fixes the code before moving on. An old
|
|
41
|
+
`datetime.now()` elsewhere in the repo was accepted in the baseline, so it doesn't block
|
|
42
|
+
anyone.
|
|
43
|
+
|
|
44
|
+
**In a live session** (Claude Code 2.1.300 with Sonnet), Claude was told to use
|
|
45
|
+
`datetime.utcnow()` *anyway*, "because our serializer expects naive UTC". It made the
|
|
46
|
+
edit and the hook blocked it. Claude named the rule, the line and the commit the lesson
|
|
47
|
+
came from. It didn't silently override the request. It asked whether to keep `utcnow()`
|
|
48
|
+
with `# nosemgrep: naive-datetime` or switch to `datetime.now(timezone.utc)`. Given the
|
|
49
|
+
same task without the insistence, Claude read the antibody's commit and wrote the
|
|
50
|
+
timezone-aware version from the start.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Not on PyPI yet: install from GitHub. The [semgrep] extra brings the engine along.
|
|
56
|
+
uv tool install "antibody[semgrep] @ git+https://github.com/cipheraxat/antibody"
|
|
57
|
+
|
|
58
|
+
cd your-repo
|
|
59
|
+
antibody init --agent claude # .antibodies/, the agent skill, and the edit hook in .claude/settings.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then, after a fix, ask your agent to *"make an antibody from that fix"*. The skill walks
|
|
63
|
+
it through these steps, which you can also run yourself:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
antibody learn # learns from uncommitted changes if there are any, else HEAD
|
|
67
|
+
antibody verify <id> # the proof; iterate on the rule until it passes
|
|
68
|
+
antibody sweep <id> # every match today: fix the real repeats...
|
|
69
|
+
antibody baseline <id> # ...and accept the rest, so only new matches fail
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Three ways to learn
|
|
73
|
+
|
|
74
|
+
| The fix is | Command | What `verify` proves |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| a commit | `antibody learn <commit>` | Fires near the change in the fix's parent, and never on lines the fix wrote |
|
|
77
|
+
| uncommitted | `antibody learn --id <name>` | The same, `HEAD` against your working tree. After you commit, `verify` pins it to the commit automatically |
|
|
78
|
+
| not in git (the agent made the mistake and fixed it in one session) | `antibody learn --fixtures-only --id <name>` | The fixture: every `ruleid:` line matches, every `ok:` line doesn't |
|
|
79
|
+
|
|
80
|
+
Tests and `.antibodies/` are left out of the proof. Matches the fix left in place are
|
|
81
|
+
reported as possible repeats.
|
|
82
|
+
|
|
83
|
+
## Only new matches fail
|
|
84
|
+
|
|
85
|
+
- **The edit hook** compares the edited file with `HEAD`. It blocks only mistakes the
|
|
86
|
+
agent just introduced, never old code it happened to touch.
|
|
87
|
+
- **`antibody baseline`** records today's accepted matches in `.antibodies/baseline.json`,
|
|
88
|
+
keyed by the matched code rather than line numbers. `antibody check` then fails only on
|
|
89
|
+
matches beyond it. A pasted copy of an accepted line still counts as new.
|
|
90
|
+
- **`antibody check --since origin/main`** looks only at files changed since a ref. That
|
|
91
|
+
suits pull requests.
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# .github/workflows/antibody.yml (pin actions to commit SHAs in real use)
|
|
95
|
+
on: [pull_request]
|
|
96
|
+
jobs:
|
|
97
|
+
antibody:
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
steps:
|
|
100
|
+
- uses: actions/checkout@v7
|
|
101
|
+
with: { fetch-depth: 0 }
|
|
102
|
+
- uses: astral-sh/setup-uv@v10
|
|
103
|
+
- run: uvx --from "antibody[semgrep] @ git+https://github.com/cipheraxat/antibody" antibody check --since origin/${{ github.base_ref }}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## The hook
|
|
107
|
+
|
|
108
|
+
`antibody init --agent claude` adds this to `.claude/settings.json`, keeping any hooks
|
|
109
|
+
already there:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{"hooks": {"PostToolUse": [{"matcher": "Edit|Write", "hooks": [
|
|
113
|
+
{"type": "command", "command": "antibody", "args": ["hook", "claude"], "timeout": 60,
|
|
114
|
+
"statusMessage": "antibody: checking edit"}]}]}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
It takes about **0.5 s** on a file an antibody covers with Semgrep 1.181, **1.4 s** with
|
|
118
|
+
Opengrep 1.31, and **0.25 s** on anything else, measured on an M-series Mac. It exits 2
|
|
119
|
+
with the message above on a new match, which Claude Code shows to Claude. It exits 0 when
|
|
120
|
+
clean, and 1 if something is broken, so a broken hook never blocks your work.
|
|
121
|
+
|
|
122
|
+
## A security example: setuptools CVE-2024-6345
|
|
123
|
+
|
|
124
|
+
setuptools formatted package URLs into `os.system` command strings. From the fix
|
|
125
|
+
[`88807c7`](https://github.com/pypa/setuptools/commit/88807c7062788254f654ea8c03427adc859321f0),
|
|
126
|
+
an agent wrote [`examples/os-system-formatted-command.yml`](examples/os-system-formatted-command.yml).
|
|
127
|
+
`antibody verify` confirmed that it catches all four vulnerable calls and none of the
|
|
128
|
+
fixed code. The sweep found no other instances in setuptools. The rule doesn't mention
|
|
129
|
+
setuptools, so it works on any Python codebase.
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
| Command | What it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `antibody init [--agent claude] [--skill]` | Create `.antibodies/`, plus the skill and edit hook |
|
|
136
|
+
| `antibody learn [commit] [--id ID] [--fixtures-only]` | Write a skeleton and print a brief for the agent |
|
|
137
|
+
| `antibody verify <id>... \| --all` | Prove each antibody; `--fix COMMIT` records the fix, `--no-stamp` leaves files alone |
|
|
138
|
+
| `antibody sweep <id> [paths]` | List every match of one antibody |
|
|
139
|
+
| `antibody baseline [ids]` | Accept today's matches |
|
|
140
|
+
| `antibody check [paths] [--since REF] [--no-baseline] [--sarif FILE]` | New matches from verified antibodies; exit 1 if any |
|
|
141
|
+
| `antibody list` | Antibodies, how each was proven, and their kind |
|
|
142
|
+
| `antibody hook claude` | The edit hook (reads Claude Code's event on stdin) |
|
|
143
|
+
|
|
144
|
+
All but `hook` take `--repo PATH`. `verify`, `sweep`, `check` and `list` take `--json`.
|
|
145
|
+
|
|
146
|
+
## The file format
|
|
147
|
+
|
|
148
|
+
An antibody is a plain Semgrep rule with its history under `metadata.antibody`, so
|
|
149
|
+
`semgrep scan --config .antibodies --exclude .antibodies` works without antibody.
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
rules:
|
|
153
|
+
- id: naive-datetime
|
|
154
|
+
message: >-
|
|
155
|
+
This makes a timestamp with no timezone ... Use datetime.now(timezone.utc).
|
|
156
|
+
severity: ERROR
|
|
157
|
+
languages: [python]
|
|
158
|
+
pattern-either:
|
|
159
|
+
- pattern: datetime.datetime.utcnow()
|
|
160
|
+
- pattern: datetime.datetime.now()
|
|
161
|
+
metadata:
|
|
162
|
+
antibody:
|
|
163
|
+
kind: bug # bug, security or convention
|
|
164
|
+
source:
|
|
165
|
+
commit: "95ffe5603abe..."
|
|
166
|
+
summary: "Use timezone-aware timestamps in billing"
|
|
167
|
+
verified: # written by `antibody verify`
|
|
168
|
+
engine: semgrep 1.181.0
|
|
169
|
+
proof: commit
|
|
170
|
+
vulnerable: 8d7f55689db5
|
|
171
|
+
fixed: 95ffe5603abe
|
|
172
|
+
fires_on:
|
|
173
|
+
- app/billing.py:5
|
|
174
|
+
at: '2026-10-18'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A file next to it with the same name and a code extension (`naive-datetime.py`) is its
|
|
178
|
+
fixture, in Semgrep's annotation style:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# ruleid: naive-datetime
|
|
182
|
+
issued = datetime.utcnow()
|
|
183
|
+
# ok: naive-datetime
|
|
184
|
+
issued = datetime.now(timezone.utc)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Engine
|
|
188
|
+
|
|
189
|
+
Antibody works with Semgrep CE and [Opengrep](https://github.com/opengrep/opengrep). The
|
|
190
|
+
full test suite passes on Semgrep 1.181 and Opengrep 1.31. It uses the engine installed
|
|
191
|
+
with it (the `[semgrep]` extra) first, then Opengrep, then Semgrep from `PATH`. Pass
|
|
192
|
+
`--engine` or set `$ANTIBODY_ENGINE` (a name or a path) to choose. Both run through their
|
|
193
|
+
fast OCaml CLI, and Semgrep always runs with `--metrics=off`. Python and
|
|
194
|
+
JavaScript/TypeScript are the first targets, and any language the engine supports works.
|
|
195
|
+
|
|
196
|
+
## Status
|
|
197
|
+
|
|
198
|
+
v0.2. Next:
|
|
199
|
+
|
|
200
|
+
- a Claude Code plugin bundling the skill and hook (after a PyPI release)
|
|
201
|
+
- blocking a bad edit *before* it's written (PreToolUse), and hooks for Cursor, Codex and opencode
|
|
202
|
+
- `learn --auto` with the Claude API, for pull-request bots
|
|
203
|
+
- snapshotting agent edits, so corrections are proven against real code rather than fixtures
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
uv sync
|
|
209
|
+
uv run pytest # runs against real git repos and the real engine (~30 s)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
[MIT](LICENSE)
|
antibody-0.2.0/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# antibody
|
|
2
|
+
|
|
3
|
+
**Every bug you fix, and every mistake you correct your coding agent on, becomes a verified
|
|
4
|
+
rule that stops it coming back.**
|
|
5
|
+
|
|
6
|
+
Agents repeat mistakes. So do teams. A note in `CLAUDE.md` is a suggestion, and the
|
|
7
|
+
lesson from a fix is usually gone the moment it merges. `antibody` keeps it. Your coding
|
|
8
|
+
agent drafts a [Semgrep](https://semgrep.dev)-format rule from the fix. `antibody` *proves*
|
|
9
|
+
the rule fires on the mistake and stays silent on the fix. It then enforces the rule on
|
|
10
|
+
every file the agent edits and in CI, with no LLM, no network and no tokens. Only new
|
|
11
|
+
matches count, so turning it on never breaks a build.
|
|
12
|
+
|
|
13
|
+
The LLM writes the check once. A deterministic tool runs it from then on.
|
|
14
|
+
|
|
15
|
+
## What it looks like
|
|
16
|
+
|
|
17
|
+
A fix made timestamps timezone-aware. Later, Claude Code writes `datetime.utcnow()` in
|
|
18
|
+
another file, and the edit hook answers right away:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
antibody: this edit repeats a mistake that was fixed before
|
|
22
|
+
app/invoices.py:5 [naive-datetime] This makes a timestamp with no timezone, which breaks comparisons and shifts times across servers. Use datetime.now(timezone.utc).
|
|
23
|
+
learned from 95ffe5603abe "Use timezone-aware timestamps in billing"
|
|
24
|
+
Fix the code. Ask the user before suppressing it with `# nosemgrep`.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Claude Code shows that message to Claude, which fixes the code before moving on. An old
|
|
28
|
+
`datetime.now()` elsewhere in the repo was accepted in the baseline, so it doesn't block
|
|
29
|
+
anyone.
|
|
30
|
+
|
|
31
|
+
**In a live session** (Claude Code 2.1.300 with Sonnet), Claude was told to use
|
|
32
|
+
`datetime.utcnow()` *anyway*, "because our serializer expects naive UTC". It made the
|
|
33
|
+
edit and the hook blocked it. Claude named the rule, the line and the commit the lesson
|
|
34
|
+
came from. It didn't silently override the request. It asked whether to keep `utcnow()`
|
|
35
|
+
with `# nosemgrep: naive-datetime` or switch to `datetime.now(timezone.utc)`. Given the
|
|
36
|
+
same task without the insistence, Claude read the antibody's commit and wrote the
|
|
37
|
+
timezone-aware version from the start.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Not on PyPI yet: install from GitHub. The [semgrep] extra brings the engine along.
|
|
43
|
+
uv tool install "antibody[semgrep] @ git+https://github.com/cipheraxat/antibody"
|
|
44
|
+
|
|
45
|
+
cd your-repo
|
|
46
|
+
antibody init --agent claude # .antibodies/, the agent skill, and the edit hook in .claude/settings.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then, after a fix, ask your agent to *"make an antibody from that fix"*. The skill walks
|
|
50
|
+
it through these steps, which you can also run yourself:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
antibody learn # learns from uncommitted changes if there are any, else HEAD
|
|
54
|
+
antibody verify <id> # the proof; iterate on the rule until it passes
|
|
55
|
+
antibody sweep <id> # every match today: fix the real repeats...
|
|
56
|
+
antibody baseline <id> # ...and accept the rest, so only new matches fail
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Three ways to learn
|
|
60
|
+
|
|
61
|
+
| The fix is | Command | What `verify` proves |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| a commit | `antibody learn <commit>` | Fires near the change in the fix's parent, and never on lines the fix wrote |
|
|
64
|
+
| uncommitted | `antibody learn --id <name>` | The same, `HEAD` against your working tree. After you commit, `verify` pins it to the commit automatically |
|
|
65
|
+
| not in git (the agent made the mistake and fixed it in one session) | `antibody learn --fixtures-only --id <name>` | The fixture: every `ruleid:` line matches, every `ok:` line doesn't |
|
|
66
|
+
|
|
67
|
+
Tests and `.antibodies/` are left out of the proof. Matches the fix left in place are
|
|
68
|
+
reported as possible repeats.
|
|
69
|
+
|
|
70
|
+
## Only new matches fail
|
|
71
|
+
|
|
72
|
+
- **The edit hook** compares the edited file with `HEAD`. It blocks only mistakes the
|
|
73
|
+
agent just introduced, never old code it happened to touch.
|
|
74
|
+
- **`antibody baseline`** records today's accepted matches in `.antibodies/baseline.json`,
|
|
75
|
+
keyed by the matched code rather than line numbers. `antibody check` then fails only on
|
|
76
|
+
matches beyond it. A pasted copy of an accepted line still counts as new.
|
|
77
|
+
- **`antibody check --since origin/main`** looks only at files changed since a ref. That
|
|
78
|
+
suits pull requests.
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
# .github/workflows/antibody.yml (pin actions to commit SHAs in real use)
|
|
82
|
+
on: [pull_request]
|
|
83
|
+
jobs:
|
|
84
|
+
antibody:
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v7
|
|
88
|
+
with: { fetch-depth: 0 }
|
|
89
|
+
- uses: astral-sh/setup-uv@v10
|
|
90
|
+
- run: uvx --from "antibody[semgrep] @ git+https://github.com/cipheraxat/antibody" antibody check --since origin/${{ github.base_ref }}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## The hook
|
|
94
|
+
|
|
95
|
+
`antibody init --agent claude` adds this to `.claude/settings.json`, keeping any hooks
|
|
96
|
+
already there:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{"hooks": {"PostToolUse": [{"matcher": "Edit|Write", "hooks": [
|
|
100
|
+
{"type": "command", "command": "antibody", "args": ["hook", "claude"], "timeout": 60,
|
|
101
|
+
"statusMessage": "antibody: checking edit"}]}]}}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It takes about **0.5 s** on a file an antibody covers with Semgrep 1.181, **1.4 s** with
|
|
105
|
+
Opengrep 1.31, and **0.25 s** on anything else, measured on an M-series Mac. It exits 2
|
|
106
|
+
with the message above on a new match, which Claude Code shows to Claude. It exits 0 when
|
|
107
|
+
clean, and 1 if something is broken, so a broken hook never blocks your work.
|
|
108
|
+
|
|
109
|
+
## A security example: setuptools CVE-2024-6345
|
|
110
|
+
|
|
111
|
+
setuptools formatted package URLs into `os.system` command strings. From the fix
|
|
112
|
+
[`88807c7`](https://github.com/pypa/setuptools/commit/88807c7062788254f654ea8c03427adc859321f0),
|
|
113
|
+
an agent wrote [`examples/os-system-formatted-command.yml`](examples/os-system-formatted-command.yml).
|
|
114
|
+
`antibody verify` confirmed that it catches all four vulnerable calls and none of the
|
|
115
|
+
fixed code. The sweep found no other instances in setuptools. The rule doesn't mention
|
|
116
|
+
setuptools, so it works on any Python codebase.
|
|
117
|
+
|
|
118
|
+
## Commands
|
|
119
|
+
|
|
120
|
+
| Command | What it does |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `antibody init [--agent claude] [--skill]` | Create `.antibodies/`, plus the skill and edit hook |
|
|
123
|
+
| `antibody learn [commit] [--id ID] [--fixtures-only]` | Write a skeleton and print a brief for the agent |
|
|
124
|
+
| `antibody verify <id>... \| --all` | Prove each antibody; `--fix COMMIT` records the fix, `--no-stamp` leaves files alone |
|
|
125
|
+
| `antibody sweep <id> [paths]` | List every match of one antibody |
|
|
126
|
+
| `antibody baseline [ids]` | Accept today's matches |
|
|
127
|
+
| `antibody check [paths] [--since REF] [--no-baseline] [--sarif FILE]` | New matches from verified antibodies; exit 1 if any |
|
|
128
|
+
| `antibody list` | Antibodies, how each was proven, and their kind |
|
|
129
|
+
| `antibody hook claude` | The edit hook (reads Claude Code's event on stdin) |
|
|
130
|
+
|
|
131
|
+
All but `hook` take `--repo PATH`. `verify`, `sweep`, `check` and `list` take `--json`.
|
|
132
|
+
|
|
133
|
+
## The file format
|
|
134
|
+
|
|
135
|
+
An antibody is a plain Semgrep rule with its history under `metadata.antibody`, so
|
|
136
|
+
`semgrep scan --config .antibodies --exclude .antibodies` works without antibody.
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
rules:
|
|
140
|
+
- id: naive-datetime
|
|
141
|
+
message: >-
|
|
142
|
+
This makes a timestamp with no timezone ... Use datetime.now(timezone.utc).
|
|
143
|
+
severity: ERROR
|
|
144
|
+
languages: [python]
|
|
145
|
+
pattern-either:
|
|
146
|
+
- pattern: datetime.datetime.utcnow()
|
|
147
|
+
- pattern: datetime.datetime.now()
|
|
148
|
+
metadata:
|
|
149
|
+
antibody:
|
|
150
|
+
kind: bug # bug, security or convention
|
|
151
|
+
source:
|
|
152
|
+
commit: "95ffe5603abe..."
|
|
153
|
+
summary: "Use timezone-aware timestamps in billing"
|
|
154
|
+
verified: # written by `antibody verify`
|
|
155
|
+
engine: semgrep 1.181.0
|
|
156
|
+
proof: commit
|
|
157
|
+
vulnerable: 8d7f55689db5
|
|
158
|
+
fixed: 95ffe5603abe
|
|
159
|
+
fires_on:
|
|
160
|
+
- app/billing.py:5
|
|
161
|
+
at: '2026-10-18'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A file next to it with the same name and a code extension (`naive-datetime.py`) is its
|
|
165
|
+
fixture, in Semgrep's annotation style:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# ruleid: naive-datetime
|
|
169
|
+
issued = datetime.utcnow()
|
|
170
|
+
# ok: naive-datetime
|
|
171
|
+
issued = datetime.now(timezone.utc)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Engine
|
|
175
|
+
|
|
176
|
+
Antibody works with Semgrep CE and [Opengrep](https://github.com/opengrep/opengrep). The
|
|
177
|
+
full test suite passes on Semgrep 1.181 and Opengrep 1.31. It uses the engine installed
|
|
178
|
+
with it (the `[semgrep]` extra) first, then Opengrep, then Semgrep from `PATH`. Pass
|
|
179
|
+
`--engine` or set `$ANTIBODY_ENGINE` (a name or a path) to choose. Both run through their
|
|
180
|
+
fast OCaml CLI, and Semgrep always runs with `--metrics=off`. Python and
|
|
181
|
+
JavaScript/TypeScript are the first targets, and any language the engine supports works.
|
|
182
|
+
|
|
183
|
+
## Status
|
|
184
|
+
|
|
185
|
+
v0.2. Next:
|
|
186
|
+
|
|
187
|
+
- a Claude Code plugin bundling the skill and hook (after a PyPI release)
|
|
188
|
+
- blocking a bad edit *before* it's written (PreToolUse), and hooks for Cursor, Codex and opencode
|
|
189
|
+
- `learn --auto` with the Claude API, for pull-request bots
|
|
190
|
+
- snapshotting agent edits, so corrections are proven against real code rather than fixtures
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
uv sync
|
|
196
|
+
uv run pytest # runs against real git repos and the real engine (~30 s)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def clone(url, filename, rev):
|
|
6
|
+
# ruleid: os-system-formatted-command
|
|
7
|
+
os.system("git clone --quiet %s %s" % (url, filename))
|
|
8
|
+
# ruleid: os-system-formatted-command
|
|
9
|
+
os.system(f"hg --cwd {filename} up -C -r {rev} -q")
|
|
10
|
+
# ruleid: os-system-formatted-command
|
|
11
|
+
subprocess.call("svn checkout {} {}".format(url, filename), shell=True)
|
|
12
|
+
# ok: os-system-formatted-command
|
|
13
|
+
subprocess.check_call(["git", "clone", "--quiet", url, filename])
|
|
14
|
+
# ok: os-system-formatted-command
|
|
15
|
+
os.system("git --version")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: os-system-formatted-command
|
|
3
|
+
message: >-
|
|
4
|
+
A shell command is built by formatting values into a string and run through a
|
|
5
|
+
shell. A URL, filename or revision containing shell metacharacters runs arbitrary
|
|
6
|
+
commands. Pass an argument list to subprocess instead.
|
|
7
|
+
severity: ERROR
|
|
8
|
+
languages: [python]
|
|
9
|
+
patterns:
|
|
10
|
+
- pattern-either:
|
|
11
|
+
- pattern: os.system($CMD)
|
|
12
|
+
- pattern: os.popen($CMD, ...)
|
|
13
|
+
- pattern: subprocess.$FN($CMD, ..., shell=True, ...)
|
|
14
|
+
- metavariable-pattern:
|
|
15
|
+
metavariable: $CMD
|
|
16
|
+
pattern-either:
|
|
17
|
+
- pattern: '"..." % $ARGS'
|
|
18
|
+
- pattern: f"..."
|
|
19
|
+
- pattern: '"...".format(...)'
|
|
20
|
+
- pattern: '"..." + $X'
|
|
21
|
+
- pattern: '$X + "..."'
|
|
22
|
+
metadata:
|
|
23
|
+
antibody:
|
|
24
|
+
source:
|
|
25
|
+
commit: 88807c7062788254f654ea8c03427adc859321f0
|
|
26
|
+
summary: "Modernize package_index VCS handling"
|
|
27
|
+
repo: "https://github.com/pypa/setuptools"
|
|
28
|
+
advisory: GHSA-cx63-2mw6-8hw5
|
|
29
|
+
cwe: CWE-78
|
|
30
|
+
verified:
|
|
31
|
+
engine: semgrep 1.181.0
|
|
32
|
+
proof: commit
|
|
33
|
+
vulnerable: 4a0a9ce58751
|
|
34
|
+
fixed: 88807c706278
|
|
35
|
+
fires_on:
|
|
36
|
+
- setuptools/package_index.py:888
|
|
37
|
+
- setuptools/package_index.py:892
|
|
38
|
+
- setuptools/package_index.py:907
|
|
39
|
+
- setuptools/package_index.py:911
|
|
40
|
+
at: '2026-10-18'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "antibody"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Turn every fix into a verified rule that stops the mistake coming back, enforced on agent edits and in CI."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"ruamel.yaml>=0.18",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Repository = "https://github.com/cipheraxat/antibody"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
# The rule engine. Skip this extra if you already have opengrep or semgrep on PATH.
|
|
18
|
+
semgrep = ["semgrep>=1.100"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
antibody = "antibody.cli:main"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8",
|
|
26
|
+
"semgrep>=1.100",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/antibody"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|