slopsentry 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slopsentry-0.1.0/.gitignore +9 -0
- slopsentry-0.1.0/LICENSE +21 -0
- slopsentry-0.1.0/PKG-INFO +235 -0
- slopsentry-0.1.0/README.md +202 -0
- slopsentry-0.1.0/RELEASING.md +25 -0
- slopsentry-0.1.0/npm/LICENSE +21 -0
- slopsentry-0.1.0/npm/README.md +54 -0
- slopsentry-0.1.0/npm/bin/slopsentry.js +44 -0
- slopsentry-0.1.0/npm/package.json +33 -0
- slopsentry-0.1.0/pyproject.toml +61 -0
- slopsentry-0.1.0/scripts/update_top_packages.py +53 -0
- slopsentry-0.1.0/slopsentry/__init__.py +3 -0
- slopsentry-0.1.0/slopsentry/checks/__init__.py +3 -0
- slopsentry-0.1.0/slopsentry/checks/age.py +74 -0
- slopsentry-0.1.0/slopsentry/checks/exists.py +17 -0
- slopsentry-0.1.0/slopsentry/checks/lookalike.py +126 -0
- slopsentry-0.1.0/slopsentry/cli.py +123 -0
- slopsentry-0.1.0/slopsentry/data/__init__.py +1 -0
- slopsentry-0.1.0/slopsentry/data/top_pypi.json +5010 -0
- slopsentry-0.1.0/slopsentry/hook.py +310 -0
- slopsentry-0.1.0/slopsentry/registry.py +62 -0
- slopsentry-0.1.0/slopsentry/scoring.py +81 -0
- slopsentry-0.1.0/slopsentry/spec.py +18 -0
- slopsentry-0.1.0/tests/__init__.py +0 -0
- slopsentry-0.1.0/tests/conftest.py +101 -0
- slopsentry-0.1.0/tests/test_age.py +52 -0
- slopsentry-0.1.0/tests/test_cli.py +136 -0
- slopsentry-0.1.0/tests/test_exists.py +36 -0
- slopsentry-0.1.0/tests/test_hook.py +215 -0
- slopsentry-0.1.0/tests/test_hook_parse.py +133 -0
- slopsentry-0.1.0/tests/test_lookalike.py +159 -0
- slopsentry-0.1.0/tests/test_registry.py +82 -0
- slopsentry-0.1.0/tests/test_scoring.py +79 -0
- slopsentry-0.1.0/tests/test_spec.py +30 -0
slopsentry-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rajeshr-developer
|
|
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.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: slopsentry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check packages for slopsquatting before they are installed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Rajeshr-developer/slopsentry
|
|
6
|
+
Project-URL: Repository, https://github.com/Rajeshr-developer/slopsentry
|
|
7
|
+
Project-URL: Issues, https://github.com/Rajeshr-developer/slopsentry/issues
|
|
8
|
+
Author-email: Rajeshr-developer <rajeshr.developer@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude-code,pypi,security,slopsquatting,supply-chain,typosquatting
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: packaging>=24
|
|
27
|
+
Requires-Dist: rich>=13
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# SlopSentry
|
|
35
|
+
|
|
36
|
+
SlopSentry checks packages for **slopsquatting** before you install them: AI-hallucinated
|
|
37
|
+
package names and malicious lookalikes that attackers register to catch them.
|
|
38
|
+
|
|
39
|
+
<!-- TODO: demo GIF of the Claude Code hook blocking a hallucinated package -->
|
|
40
|
+
<!--  -->
|
|
41
|
+
|
|
42
|
+
> **Alpha (0.1.0):** only Python package installs from PyPI are checked. npm and other
|
|
43
|
+
> ecosystems aren't supported yet.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
SlopSentry needs Python 3.10 or newer. Install it as a standalone tool with
|
|
48
|
+
[pipx](https://pipx.pypa.io/):
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pipx install slopsentry
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
or run it without installing, using [uv](https://docs.astral.sh/uv/):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uvx slopsentry check requests
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`pip install slopsentry` works too.
|
|
61
|
+
|
|
62
|
+
For development:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
python3 -m venv .venv
|
|
66
|
+
.venv/bin/pip install -e ".[dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
slopsentry check requests
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`check` runs every check, adds up the risk points, and gives a verdict:
|
|
76
|
+
|
|
77
|
+
| Check | Flags | Points |
|
|
78
|
+
| --------- | ----------------------------------------------------- | ------ |
|
|
79
|
+
| exists | package not found on PyPI | 100 |
|
|
80
|
+
| age | first release under 90 days ago, or no released files | 40 |
|
|
81
|
+
| age | two or fewer releases | 20 |
|
|
82
|
+
| age | no homepage or repository link | 20 |
|
|
83
|
+
| lookalike | resembles a popular package (see below) | 50 |
|
|
84
|
+
| lookalike | resembles one, but the age check passes cleanly | 15 |
|
|
85
|
+
|
|
86
|
+
| Verdict | Total points | Exit code |
|
|
87
|
+
| ------- | ------------ | --------- |
|
|
88
|
+
| PASS | under 30 | 0 |
|
|
89
|
+
| WARN | 30 to 69 | 0 |
|
|
90
|
+
| BLOCK | 70 or more | 1 |
|
|
91
|
+
|
|
92
|
+
If PyPI can't be reached, the package can't be verified: the verdict is at least WARN and
|
|
93
|
+
the exit code 0, or ERROR with exit code 2 under `--strict`. Empty or invalid input is an
|
|
94
|
+
ERROR with exit code 2. Thresholds live in `slopsentry/scoring.py`.
|
|
95
|
+
|
|
96
|
+
Without `--explain`, `check` prints one summary line plus the reasons for a WARN or
|
|
97
|
+
BLOCK. With `--explain` it prints a table of every check's status, detail and points:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
slopsentry check --explain requests
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Lookalike check
|
|
104
|
+
|
|
105
|
+
The lookalike check compares the name with the 5,000 most-downloaded PyPI packages,
|
|
106
|
+
bundled in `slopsentry/data/top_pypi.json`. Popular packages are never flagged, so
|
|
107
|
+
`requests` passes even though it's close to other names. A name is flagged if:
|
|
108
|
+
|
|
109
|
+
- it differs from a popular name only by separators (`django-rest-framework` vs
|
|
110
|
+
`djangorestframework`),
|
|
111
|
+
- it adds or removes `py-`, `python-`, `-py`, `-python` or `-lib` (`python-requests`,
|
|
112
|
+
`pandas-py`), or
|
|
113
|
+
- it's within Damerau-Levenshtein distance 1 of a popular name, or 2 for names longer
|
|
114
|
+
than 10 characters (`reqeusts`, `numpyy`).
|
|
115
|
+
|
|
116
|
+
Two guards cut false positives on legitimate packages:
|
|
117
|
+
|
|
118
|
+
1. **Short names:** names under 5 characters get no edit-distance matching, since almost
|
|
119
|
+
every short name is one edit from another (`sax`/`six`, `od`/`id`).
|
|
120
|
+
2. **Package families:** a distance-2 match is ignored when the names differ in exactly
|
|
121
|
+
one whole `-` segment, such as `mypy-boto3-emr` and `mypy-boto3-ec2`.
|
|
122
|
+
|
|
123
|
+
The reason names the package it resembles, e.g. "looks like requests". If the package is
|
|
124
|
+
established (the age check passes cleanly), the match adds only 15 points and reads
|
|
125
|
+
"looks like requests (established package)": shown in `--explain`, but not enough to warn
|
|
126
|
+
by itself. A new or thin lookalike adds 50, so one that is also brand new blocks.
|
|
127
|
+
|
|
128
|
+
To refresh the bundled list (it needs the network, or a downloaded copy of the dataset):
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
.venv/bin/python scripts/update_top_packages.py
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`PACKAGE` can be a full requirement: `requests==2.31.0` and `requests[socks]` are
|
|
135
|
+
checked as `requests`. Invalid input is rejected before any request is made.
|
|
136
|
+
|
|
137
|
+
Names are normalised per PEP 503, so `Requests` and `typing_extensions` match
|
|
138
|
+
`requests` and `typing-extensions`.
|
|
139
|
+
|
|
140
|
+
Use `--strict` to treat an unreachable registry as a failure:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
slopsentry check --strict requests
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Claude Code hook
|
|
147
|
+
|
|
148
|
+
`slopsentry hook` is a Claude Code `PreToolUse` hook. It reads the hook event on stdin,
|
|
149
|
+
finds install commands in Bash tool calls, scores each package, and answers on stdout:
|
|
150
|
+
|
|
151
|
+
| Worst verdict | Decision |
|
|
152
|
+
| ------------- | ------------------------------------------------ |
|
|
153
|
+
| BLOCK | `deny`, with the package, score and reasons |
|
|
154
|
+
| WARN | `ask`, so you confirm before it runs |
|
|
155
|
+
| PASS | allowed silently |
|
|
156
|
+
|
|
157
|
+
It recognises `pip install` (also `pip3`, `python -m pip`, `python3 -m pip`), `uv add`,
|
|
158
|
+
`uv pip install` and `poetry add`, including in chained commands (`&&`, `||`, `;`, `|`)
|
|
159
|
+
and after `sudo`, `env` or `VAR=value` prefixes. Flags, local paths, URLs, `-e` and `-r`
|
|
160
|
+
arguments are skipped. Commands that use a custom index (`--index-url`, `-i`,
|
|
161
|
+
`--extra-index-url`, `-f`/`--find-links`, uv's `--index`/`--default-index`, poetry's
|
|
162
|
+
`--source`) aren't checked, since their packages may not be on PyPI. In 0.1.0 only
|
|
163
|
+
Python installs are checked; `npm install`, `yarn add` and other ecosystems pass through
|
|
164
|
+
unchecked.
|
|
165
|
+
|
|
166
|
+
The hook fails open: it always exits 0. Commands it can't parse, registry errors (5 second
|
|
167
|
+
timeout) and its own crashes all allow the command. Crashes are appended with a timestamp
|
|
168
|
+
to `~/.slopsentry/hook.log`; add `--debug` to also print them to stderr.
|
|
169
|
+
|
|
170
|
+
### Setting up the hook
|
|
171
|
+
|
|
172
|
+
1. Install SlopSentry so `slopsentry` is on your `PATH`:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
pipx install slopsentry
|
|
176
|
+
slopsentry --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
2. Add the hook to `.claude/settings.json` in a project, or to `~/.claude/settings.json`
|
|
180
|
+
to cover every project:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"hooks": {
|
|
185
|
+
"PreToolUse": [
|
|
186
|
+
{
|
|
187
|
+
"matcher": "Bash",
|
|
188
|
+
"hooks": [
|
|
189
|
+
{
|
|
190
|
+
"type": "command",
|
|
191
|
+
"command": "slopsentry hook || true",
|
|
192
|
+
"timeout": 30
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`|| true` keeps Claude Code working if `slopsentry` isn't installed on a machine
|
|
202
|
+
where the settings file is shared.
|
|
203
|
+
|
|
204
|
+
3. Restart Claude Code, then check it in `/hooks`. Asking Claude to run
|
|
205
|
+
`pip install reqeusts` should be blocked.
|
|
206
|
+
|
|
207
|
+
If you use uv and don't want to install anything, use `"command": "uvx slopsentry hook || true"`
|
|
208
|
+
instead. The first run downloads SlopSentry, so it's slower.
|
|
209
|
+
|
|
210
|
+
When working on SlopSentry itself, point the hook at your checkout instead:
|
|
211
|
+
`"\"$CLAUDE_PROJECT_DIR\"/.venv/bin/slopsentry hook || true"`.
|
|
212
|
+
|
|
213
|
+
### Known gaps
|
|
214
|
+
|
|
215
|
+
The hook reads the command text; it doesn't run a shell. These installs get past it:
|
|
216
|
+
|
|
217
|
+
- **`bash -c "pip install x"`** (and `sh -c`, `eval`): the install is inside a string
|
|
218
|
+
argument that isn't parsed.
|
|
219
|
+
- **`$(...)` and backtick subshells**, e.g. `pip install $(cat pkgs.txt)`: the package
|
|
220
|
+
names only exist once the shell runs.
|
|
221
|
+
- **`xargs`**, e.g. `cat pkgs.txt | xargs pip install`: the names arrive on stdin.
|
|
222
|
+
- **`-r requirements.txt`**: requirement files aren't read, so their packages aren't
|
|
223
|
+
checked.
|
|
224
|
+
- **`--extra-index-url` dependency confusion**: commands with an extra index are skipped
|
|
225
|
+
entirely. pip picks the highest version across all indexes, so an attacker can publish
|
|
226
|
+
your private package's name on PyPI with a higher version, and it wins.
|
|
227
|
+
|
|
228
|
+
## Data attribution
|
|
229
|
+
|
|
230
|
+
`slopsentry/data/top_pypi.json` is derived from
|
|
231
|
+
[Top PyPI Packages](https://hugovk.dev/top-pypi-packages/) by Hugo van Kemenade and
|
|
232
|
+
contributors ([DOI 10.5281/zenodo.23082845](https://doi.org/10.5281/zenodo.23082845)),
|
|
233
|
+
release 2026.10.1, licensed under
|
|
234
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Changes: kept the top 5,000
|
|
235
|
+
names in download order, normalised them per PEP 503, and removed the download counts.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# SlopSentry
|
|
2
|
+
|
|
3
|
+
SlopSentry checks packages for **slopsquatting** before you install them: AI-hallucinated
|
|
4
|
+
package names and malicious lookalikes that attackers register to catch them.
|
|
5
|
+
|
|
6
|
+
<!-- TODO: demo GIF of the Claude Code hook blocking a hallucinated package -->
|
|
7
|
+
<!--  -->
|
|
8
|
+
|
|
9
|
+
> **Alpha (0.1.0):** only Python package installs from PyPI are checked. npm and other
|
|
10
|
+
> ecosystems aren't supported yet.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
SlopSentry needs Python 3.10 or newer. Install it as a standalone tool with
|
|
15
|
+
[pipx](https://pipx.pypa.io/):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pipx install slopsentry
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or run it without installing, using [uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uvx slopsentry check requests
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`pip install slopsentry` works too.
|
|
28
|
+
|
|
29
|
+
For development:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
python3 -m venv .venv
|
|
33
|
+
.venv/bin/pip install -e ".[dev]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
slopsentry check requests
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`check` runs every check, adds up the risk points, and gives a verdict:
|
|
43
|
+
|
|
44
|
+
| Check | Flags | Points |
|
|
45
|
+
| --------- | ----------------------------------------------------- | ------ |
|
|
46
|
+
| exists | package not found on PyPI | 100 |
|
|
47
|
+
| age | first release under 90 days ago, or no released files | 40 |
|
|
48
|
+
| age | two or fewer releases | 20 |
|
|
49
|
+
| age | no homepage or repository link | 20 |
|
|
50
|
+
| lookalike | resembles a popular package (see below) | 50 |
|
|
51
|
+
| lookalike | resembles one, but the age check passes cleanly | 15 |
|
|
52
|
+
|
|
53
|
+
| Verdict | Total points | Exit code |
|
|
54
|
+
| ------- | ------------ | --------- |
|
|
55
|
+
| PASS | under 30 | 0 |
|
|
56
|
+
| WARN | 30 to 69 | 0 |
|
|
57
|
+
| BLOCK | 70 or more | 1 |
|
|
58
|
+
|
|
59
|
+
If PyPI can't be reached, the package can't be verified: the verdict is at least WARN and
|
|
60
|
+
the exit code 0, or ERROR with exit code 2 under `--strict`. Empty or invalid input is an
|
|
61
|
+
ERROR with exit code 2. Thresholds live in `slopsentry/scoring.py`.
|
|
62
|
+
|
|
63
|
+
Without `--explain`, `check` prints one summary line plus the reasons for a WARN or
|
|
64
|
+
BLOCK. With `--explain` it prints a table of every check's status, detail and points:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
slopsentry check --explain requests
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Lookalike check
|
|
71
|
+
|
|
72
|
+
The lookalike check compares the name with the 5,000 most-downloaded PyPI packages,
|
|
73
|
+
bundled in `slopsentry/data/top_pypi.json`. Popular packages are never flagged, so
|
|
74
|
+
`requests` passes even though it's close to other names. A name is flagged if:
|
|
75
|
+
|
|
76
|
+
- it differs from a popular name only by separators (`django-rest-framework` vs
|
|
77
|
+
`djangorestframework`),
|
|
78
|
+
- it adds or removes `py-`, `python-`, `-py`, `-python` or `-lib` (`python-requests`,
|
|
79
|
+
`pandas-py`), or
|
|
80
|
+
- it's within Damerau-Levenshtein distance 1 of a popular name, or 2 for names longer
|
|
81
|
+
than 10 characters (`reqeusts`, `numpyy`).
|
|
82
|
+
|
|
83
|
+
Two guards cut false positives on legitimate packages:
|
|
84
|
+
|
|
85
|
+
1. **Short names:** names under 5 characters get no edit-distance matching, since almost
|
|
86
|
+
every short name is one edit from another (`sax`/`six`, `od`/`id`).
|
|
87
|
+
2. **Package families:** a distance-2 match is ignored when the names differ in exactly
|
|
88
|
+
one whole `-` segment, such as `mypy-boto3-emr` and `mypy-boto3-ec2`.
|
|
89
|
+
|
|
90
|
+
The reason names the package it resembles, e.g. "looks like requests". If the package is
|
|
91
|
+
established (the age check passes cleanly), the match adds only 15 points and reads
|
|
92
|
+
"looks like requests (established package)": shown in `--explain`, but not enough to warn
|
|
93
|
+
by itself. A new or thin lookalike adds 50, so one that is also brand new blocks.
|
|
94
|
+
|
|
95
|
+
To refresh the bundled list (it needs the network, or a downloaded copy of the dataset):
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
.venv/bin/python scripts/update_top_packages.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`PACKAGE` can be a full requirement: `requests==2.31.0` and `requests[socks]` are
|
|
102
|
+
checked as `requests`. Invalid input is rejected before any request is made.
|
|
103
|
+
|
|
104
|
+
Names are normalised per PEP 503, so `Requests` and `typing_extensions` match
|
|
105
|
+
`requests` and `typing-extensions`.
|
|
106
|
+
|
|
107
|
+
Use `--strict` to treat an unreachable registry as a failure:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
slopsentry check --strict requests
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Claude Code hook
|
|
114
|
+
|
|
115
|
+
`slopsentry hook` is a Claude Code `PreToolUse` hook. It reads the hook event on stdin,
|
|
116
|
+
finds install commands in Bash tool calls, scores each package, and answers on stdout:
|
|
117
|
+
|
|
118
|
+
| Worst verdict | Decision |
|
|
119
|
+
| ------------- | ------------------------------------------------ |
|
|
120
|
+
| BLOCK | `deny`, with the package, score and reasons |
|
|
121
|
+
| WARN | `ask`, so you confirm before it runs |
|
|
122
|
+
| PASS | allowed silently |
|
|
123
|
+
|
|
124
|
+
It recognises `pip install` (also `pip3`, `python -m pip`, `python3 -m pip`), `uv add`,
|
|
125
|
+
`uv pip install` and `poetry add`, including in chained commands (`&&`, `||`, `;`, `|`)
|
|
126
|
+
and after `sudo`, `env` or `VAR=value` prefixes. Flags, local paths, URLs, `-e` and `-r`
|
|
127
|
+
arguments are skipped. Commands that use a custom index (`--index-url`, `-i`,
|
|
128
|
+
`--extra-index-url`, `-f`/`--find-links`, uv's `--index`/`--default-index`, poetry's
|
|
129
|
+
`--source`) aren't checked, since their packages may not be on PyPI. In 0.1.0 only
|
|
130
|
+
Python installs are checked; `npm install`, `yarn add` and other ecosystems pass through
|
|
131
|
+
unchecked.
|
|
132
|
+
|
|
133
|
+
The hook fails open: it always exits 0. Commands it can't parse, registry errors (5 second
|
|
134
|
+
timeout) and its own crashes all allow the command. Crashes are appended with a timestamp
|
|
135
|
+
to `~/.slopsentry/hook.log`; add `--debug` to also print them to stderr.
|
|
136
|
+
|
|
137
|
+
### Setting up the hook
|
|
138
|
+
|
|
139
|
+
1. Install SlopSentry so `slopsentry` is on your `PATH`:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
pipx install slopsentry
|
|
143
|
+
slopsentry --help
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. Add the hook to `.claude/settings.json` in a project, or to `~/.claude/settings.json`
|
|
147
|
+
to cover every project:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"hooks": {
|
|
152
|
+
"PreToolUse": [
|
|
153
|
+
{
|
|
154
|
+
"matcher": "Bash",
|
|
155
|
+
"hooks": [
|
|
156
|
+
{
|
|
157
|
+
"type": "command",
|
|
158
|
+
"command": "slopsentry hook || true",
|
|
159
|
+
"timeout": 30
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`|| true` keeps Claude Code working if `slopsentry` isn't installed on a machine
|
|
169
|
+
where the settings file is shared.
|
|
170
|
+
|
|
171
|
+
3. Restart Claude Code, then check it in `/hooks`. Asking Claude to run
|
|
172
|
+
`pip install reqeusts` should be blocked.
|
|
173
|
+
|
|
174
|
+
If you use uv and don't want to install anything, use `"command": "uvx slopsentry hook || true"`
|
|
175
|
+
instead. The first run downloads SlopSentry, so it's slower.
|
|
176
|
+
|
|
177
|
+
When working on SlopSentry itself, point the hook at your checkout instead:
|
|
178
|
+
`"\"$CLAUDE_PROJECT_DIR\"/.venv/bin/slopsentry hook || true"`.
|
|
179
|
+
|
|
180
|
+
### Known gaps
|
|
181
|
+
|
|
182
|
+
The hook reads the command text; it doesn't run a shell. These installs get past it:
|
|
183
|
+
|
|
184
|
+
- **`bash -c "pip install x"`** (and `sh -c`, `eval`): the install is inside a string
|
|
185
|
+
argument that isn't parsed.
|
|
186
|
+
- **`$(...)` and backtick subshells**, e.g. `pip install $(cat pkgs.txt)`: the package
|
|
187
|
+
names only exist once the shell runs.
|
|
188
|
+
- **`xargs`**, e.g. `cat pkgs.txt | xargs pip install`: the names arrive on stdin.
|
|
189
|
+
- **`-r requirements.txt`**: requirement files aren't read, so their packages aren't
|
|
190
|
+
checked.
|
|
191
|
+
- **`--extra-index-url` dependency confusion**: commands with an extra index are skipped
|
|
192
|
+
entirely. pip picks the highest version across all indexes, so an attacker can publish
|
|
193
|
+
your private package's name on PyPI with a higher version, and it wins.
|
|
194
|
+
|
|
195
|
+
## Data attribution
|
|
196
|
+
|
|
197
|
+
`slopsentry/data/top_pypi.json` is derived from
|
|
198
|
+
[Top PyPI Packages](https://hugovk.dev/top-pypi-packages/) by Hugo van Kemenade and
|
|
199
|
+
contributors ([DOI 10.5281/zenodo.23082845](https://doi.org/10.5281/zenodo.23082845)),
|
|
200
|
+
release 2026.10.1, licensed under
|
|
201
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Changes: kept the top 5,000
|
|
202
|
+
names in download order, normalised them per PEP 503, and removed the download counts.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
1. **Bump the version in both places, together:**
|
|
4
|
+
- `pyproject.toml` → `[project] version`
|
|
5
|
+
- `npm/package.json` → `version`
|
|
6
|
+
|
|
7
|
+
They must match. The npm wrapper runs `uvx slopsentry@<its own version>`, so an npm
|
|
8
|
+
release whose version isn't on PyPI fails for every user.
|
|
9
|
+
2. Run `pytest` and `ruff check`, and confirm CI is green on `main`.
|
|
10
|
+
3. Build and check locally: `python -m build` and `twine check --strict dist/*`.
|
|
11
|
+
4. Publish to TestPyPI: run the **Publish** workflow manually (Actions → Publish → Run
|
|
12
|
+
workflow, or `gh workflow run publish.yml`). Then install it into a fresh venv:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ slopsentry==X.Y.Z
|
|
16
|
+
slopsentry check requests
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
5. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`. The Publish workflow checks
|
|
20
|
+
the tag matches `pyproject.toml` and publishes to PyPI.
|
|
21
|
+
6. Once the version is live on PyPI, publish the npm wrapper: `cd npm && npm publish`.
|
|
22
|
+
Check it with `npx slopsentry@X.Y.Z check requests`.
|
|
23
|
+
|
|
24
|
+
Trusted Publishing is configured on pypi.org and test.pypi.org for this repo, workflow
|
|
25
|
+
`publish.yml`, with environments `pypi` and `testpypi`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rajeshr-developer
|
|
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.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# slopsentry (npm wrapper)
|
|
2
|
+
|
|
3
|
+
This package is a thin wrapper around [SlopSentry](https://github.com/Rajeshr-developer/slopsentry),
|
|
4
|
+
a Python CLI that checks packages for **slopsquatting** (AI-hallucinated package names and
|
|
5
|
+
malicious lookalikes) before they're installed.
|
|
6
|
+
|
|
7
|
+
It contains no checking logic of its own. The `slopsentry` command runs
|
|
8
|
+
`uvx slopsentry@<version>` and passes every argument, stdin, stdout and the exit code
|
|
9
|
+
straight through, so it behaves exactly like the Python tool.
|
|
10
|
+
|
|
11
|
+
The Python version is pinned to this package's version: `slopsentry@0.1.0` on npm always
|
|
12
|
+
runs `slopsentry==0.1.0` from PyPI, never whatever uv happens to have cached.
|
|
13
|
+
|
|
14
|
+
> **Alpha (0.1.0):** SlopSentry only checks Python package installs from PyPI. It doesn't
|
|
15
|
+
> check npm packages yet, despite being installable from npm.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 18 or newer
|
|
20
|
+
- [uv](https://docs.astral.sh/uv/getting-started/installation/), which provides `uvx`.
|
|
21
|
+
uv downloads Python and SlopSentry on first run, so the first call is slower.
|
|
22
|
+
|
|
23
|
+
If `uvx` isn't on your `PATH`, the wrapper prints install instructions and exits with
|
|
24
|
+
code 127.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx slopsentry check requests
|
|
30
|
+
npx slopsentry check --explain reqeusts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
or install it globally:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install -g slopsentry
|
|
37
|
+
slopsentry check requests
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
As a Claude Code `PreToolUse` hook, use `"command": "npx -y slopsentry hook || true"`.
|
|
41
|
+
See the [main README](https://github.com/Rajeshr-developer/slopsentry#claude-code-hook)
|
|
42
|
+
for the full setup and what the hook checks.
|
|
43
|
+
|
|
44
|
+
## Prefer the Python package
|
|
45
|
+
|
|
46
|
+
If you have Python, installing it directly avoids the Node and uv layers:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pipx install slopsentry
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin wrapper: runs the Python SlopSentry CLI through uvx, passing every argument through.
|
|
3
|
+
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
const { version } = require("../package.json");
|
|
8
|
+
|
|
9
|
+
// Pinned to the npm package version, so each npm release runs the matching PyPI release.
|
|
10
|
+
const pkg = `slopsentry@${version}`;
|
|
11
|
+
|
|
12
|
+
const child = spawn("uvx", [pkg, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
13
|
+
|
|
14
|
+
child.on("error", (err) => {
|
|
15
|
+
if (err.code === "ENOENT") {
|
|
16
|
+
console.error(
|
|
17
|
+
[
|
|
18
|
+
"slopsentry: uv is not installed (the `uvx` command wasn't found on PATH).",
|
|
19
|
+
"",
|
|
20
|
+
"This npm package is a wrapper that runs the Python tool with uv. Install uv:",
|
|
21
|
+
" macOS / Linux: curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
22
|
+
' Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"',
|
|
23
|
+
" or see https://docs.astral.sh/uv/getting-started/installation/",
|
|
24
|
+
"",
|
|
25
|
+
"Or install SlopSentry directly with: pipx install slopsentry",
|
|
26
|
+
].join("\n"),
|
|
27
|
+
);
|
|
28
|
+
process.exit(127);
|
|
29
|
+
}
|
|
30
|
+
console.error(`slopsentry: failed to run uvx: ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
35
|
+
process.on(signal, () => child.kill(signal));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
child.on("exit", (code, signal) => {
|
|
39
|
+
if (signal) {
|
|
40
|
+
process.kill(process.pid, signal);
|
|
41
|
+
} else {
|
|
42
|
+
process.exit(code ?? 1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slopsentry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Check packages for slopsquatting before they are installed. npm wrapper for the Python SlopSentry CLI (runs it with uvx).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"security",
|
|
7
|
+
"supply-chain",
|
|
8
|
+
"slopsquatting",
|
|
9
|
+
"typosquatting",
|
|
10
|
+
"claude-code"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/Rajeshr-developer/slopsentry",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Rajeshr-developer/slopsentry/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Rajeshr-developer/slopsentry.git",
|
|
19
|
+
"directory": "npm"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Rajeshr-developer <rajeshr.developer@gmail.com>",
|
|
23
|
+
"bin": {
|
|
24
|
+
"slopsentry": "bin/slopsentry.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|