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.
Files changed (34) hide show
  1. slopsentry-0.1.0/.gitignore +9 -0
  2. slopsentry-0.1.0/LICENSE +21 -0
  3. slopsentry-0.1.0/PKG-INFO +235 -0
  4. slopsentry-0.1.0/README.md +202 -0
  5. slopsentry-0.1.0/RELEASING.md +25 -0
  6. slopsentry-0.1.0/npm/LICENSE +21 -0
  7. slopsentry-0.1.0/npm/README.md +54 -0
  8. slopsentry-0.1.0/npm/bin/slopsentry.js +44 -0
  9. slopsentry-0.1.0/npm/package.json +33 -0
  10. slopsentry-0.1.0/pyproject.toml +61 -0
  11. slopsentry-0.1.0/scripts/update_top_packages.py +53 -0
  12. slopsentry-0.1.0/slopsentry/__init__.py +3 -0
  13. slopsentry-0.1.0/slopsentry/checks/__init__.py +3 -0
  14. slopsentry-0.1.0/slopsentry/checks/age.py +74 -0
  15. slopsentry-0.1.0/slopsentry/checks/exists.py +17 -0
  16. slopsentry-0.1.0/slopsentry/checks/lookalike.py +126 -0
  17. slopsentry-0.1.0/slopsentry/cli.py +123 -0
  18. slopsentry-0.1.0/slopsentry/data/__init__.py +1 -0
  19. slopsentry-0.1.0/slopsentry/data/top_pypi.json +5010 -0
  20. slopsentry-0.1.0/slopsentry/hook.py +310 -0
  21. slopsentry-0.1.0/slopsentry/registry.py +62 -0
  22. slopsentry-0.1.0/slopsentry/scoring.py +81 -0
  23. slopsentry-0.1.0/slopsentry/spec.py +18 -0
  24. slopsentry-0.1.0/tests/__init__.py +0 -0
  25. slopsentry-0.1.0/tests/conftest.py +101 -0
  26. slopsentry-0.1.0/tests/test_age.py +52 -0
  27. slopsentry-0.1.0/tests/test_cli.py +136 -0
  28. slopsentry-0.1.0/tests/test_exists.py +36 -0
  29. slopsentry-0.1.0/tests/test_hook.py +215 -0
  30. slopsentry-0.1.0/tests/test_hook_parse.py +133 -0
  31. slopsentry-0.1.0/tests/test_lookalike.py +159 -0
  32. slopsentry-0.1.0/tests/test_registry.py +82 -0
  33. slopsentry-0.1.0/tests/test_scoring.py +79 -0
  34. slopsentry-0.1.0/tests/test_spec.py +30 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ .DS_Store
@@ -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
+ <!-- ![SlopSentry blocking a hallucinated package in Claude Code](docs/demo.gif) -->
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
+ <!-- ![SlopSentry blocking a hallucinated package in Claude Code](docs/demo.gif) -->
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
+ }