slopsquat-guard 0.4.1__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.
- slopsquat_guard-0.4.1/LICENSE +21 -0
- slopsquat_guard-0.4.1/PKG-INFO +138 -0
- slopsquat_guard-0.4.1/README.md +126 -0
- slopsquat_guard-0.4.1/pkg_guard.py +1749 -0
- slopsquat_guard-0.4.1/pyproject.toml +21 -0
- slopsquat_guard-0.4.1/setup.cfg +4 -0
- slopsquat_guard-0.4.1/slopsquat_guard.egg-info/PKG-INFO +138 -0
- slopsquat_guard-0.4.1/slopsquat_guard.egg-info/SOURCES.txt +9 -0
- slopsquat_guard-0.4.1/slopsquat_guard.egg-info/dependency_links.txt +1 -0
- slopsquat_guard-0.4.1/slopsquat_guard.egg-info/entry_points.txt +2 -0
- slopsquat_guard-0.4.1/slopsquat_guard.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Somesh Ghaturle
|
|
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,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slopsquat-guard
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Block hallucinated & typosquatted package installs by AI coding agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/somesh-ghaturle/pkg-guard
|
|
7
|
+
Project-URL: Issues, https://github.com/somesh-ghaturle/pkg-guard/issues
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# pkg-guard
|
|
14
|
+
|
|
15
|
+
AI coding agents sometimes install packages that don't exist. Attackers register
|
|
16
|
+
those hallucinated names, plus one-letter typos of popular packages, and wait
|
|
17
|
+
("slopsquatting"). **pkg-guard** is a hook that checks every install command your
|
|
18
|
+
agent runs and blocks the suspicious ones before they execute.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
$ pkg-guard check "pip install reqeusts"
|
|
22
|
+
BLOCK reqeusts: does not exist on pypi (hallucinated name?). Did you mean 'requests'?
|
|
23
|
+
|
|
24
|
+
$ pkg-guard check "npm i lodahs"
|
|
25
|
+
BLOCK lodahs: looks like a typosquat of popular package 'lodash'.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The agent sees the reason and fixes its own command.
|
|
29
|
+
|
|
30
|
+
- **Works with** Claude Code, Codex, and Gemini CLI. One script speaks all three hook protocols.
|
|
31
|
+
- **Covers** npm, pnpm, yarn, bun, npx, pip, uv, uvx, poetry, pipx, cargo, gem, and go (`go get`, `go install`, `go run mod@v`).
|
|
32
|
+
- **Single file, Python stdlib only.** The guard adds no dependencies of its own. Read it in five minutes.
|
|
33
|
+
|
|
34
|
+
## What gets blocked
|
|
35
|
+
|
|
36
|
+
| Signal | Rule |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Doesn't exist | Registry returns 404. Suggests the popular name it resembles. |
|
|
39
|
+
| Too new | First published less than 30 days ago (`PKG_GUARD_MIN_AGE_DAYS`) |
|
|
40
|
+
| Too few users | npm: fewer than 100 weekly downloads. crates: fewer than 100 in 90 days. gems: fewer than 1,000 total. Go (GitHub/GitLab/Bitbucket): fewer than 10 repo stars, since Go publish times come from git commits and can be backdated. |
|
|
41
|
+
| Typosquat | One edit away from a popular package, and young or little-used |
|
|
42
|
+
| Install script (npm) | Latest version runs `preinstall`/`install`/`postinstall` code, and the package is young (< 180 days) or little-used (< 10,000 weekly downloads) |
|
|
43
|
+
|
|
44
|
+
It also catches the usual ways around a naive regex: `bash -lc "..."`, `$(...)`,
|
|
45
|
+
`sudo`/`env` wrappers, `npm isntall`, `npm i x@npm:evil`, `npx -p evil`,
|
|
46
|
+
`uvx --with evil`, and `pip install -r requirements.txt` (nested `-r` included).
|
|
47
|
+
|
|
48
|
+
It also checks file edits (Claude Code `Write`/`Edit`, Codex `apply_patch`, Gemini CLI `write_file`/`replace`): dependencies the agent adds to
|
|
49
|
+
`package.json`, `requirements*.txt`, `pyproject.toml`, `Cargo.toml`, `Gemfile`, or `go.mod`
|
|
50
|
+
are checked before the edit is written, so a later bare `npm install` can't sneak them in.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
### Claude Code (plugin: hook + skill)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/plugin marketplace add somesh-ghaturle/pkg-guard
|
|
58
|
+
/plugin install pkg-guard@pkg-guard
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Codex
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/somesh-ghaturle/pkg-guard ~/.pkg-guard
|
|
65
|
+
mkdir -p ~/.codex/skills && cp -r ~/.pkg-guard/skills/pkg-guard ~/.codex/skills/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then add the hook to `~/.codex/config.toml`, or use [`examples/codex-hooks.json`](examples/codex-hooks.json) as `~/.codex/hooks.json`:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[[hooks.PreToolUse]]
|
|
72
|
+
matcher = "Bash|apply_patch"
|
|
73
|
+
|
|
74
|
+
[[hooks.PreToolUse.hooks]]
|
|
75
|
+
type = "command"
|
|
76
|
+
command = "python3 ~/.pkg-guard/pkg_guard.py hook"
|
|
77
|
+
timeout = 20
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Gemini CLI
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/somesh-ghaturle/pkg-guard ~/.pkg-guard
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then merge [`examples/gemini-settings.json`](examples/gemini-settings.json) into `~/.gemini/settings.json`, with the command set to `python3 ~/.pkg-guard/pkg_guard.py hook`. It checks `run_shell_command` plus `write_file`/`replace` edits to manifests.
|
|
87
|
+
|
|
88
|
+
### Claude Code without the plugin
|
|
89
|
+
|
|
90
|
+
Add [`examples/claude-settings.json`](examples/claude-settings.json) to `~/.claude/settings.json`, using your own path to the script.
|
|
91
|
+
|
|
92
|
+
### As a CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pipx install slopsquat-guard # the PyPI name; the command is pkg-guard
|
|
96
|
+
pkg-guard check "npm install some-lib" # exit 1 if blocked
|
|
97
|
+
pkg-guard scan # check every package in ./package-lock.json, uv.lock, ...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The PyPI package is **`slopsquat-guard`**. `pkg-guard` on PyPI is a different, unrelated project.
|
|
101
|
+
|
|
102
|
+
`scan` reads `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock` (classic and Berry),
|
|
103
|
+
`pnpm-lock.yaml`, `uv.lock`, `poetry.lock`, `Cargo.lock` (the TOML ones need Python 3.11+),
|
|
104
|
+
`Gemfile.lock` and `go.sum`, skips git/path/workspace entries, and
|
|
105
|
+
applies the same rules to every registry package, transitive ones included. Use it once
|
|
106
|
+
when you add pkg-guard to an existing repo, or in CI.
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
| Env var | Effect |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `PKG_GUARD_ALLOW=a,b` | Always allow these packages (internal or private names) |
|
|
113
|
+
| `PKG_GUARD_MIN_AGE_DAYS=30` | Minimum package age |
|
|
114
|
+
| `GOPRIVATE` / `GONOPROXY` | Go modules matching these globs are skipped, like `go` does (env vars only, not `go env -w`) |
|
|
115
|
+
| `XDG_CACHE_HOME` | Packages that pass are cached for 24h in `$XDG_CACHE_HOME/pkg-guard/pkg-guard-ok.json` (default `~/.cache`). Blocks are never cached. Delete the file to recheck everything. |
|
|
116
|
+
| `PKG_GUARD_FAIL_CLOSED=1` | Block when the registry can't be reached. The default is to allow and warn, so offline work keeps going. |
|
|
117
|
+
|
|
118
|
+
## Limits
|
|
119
|
+
|
|
120
|
+
pkg-guard catches agent **mistakes**: hallucinated names, typos, packages too new
|
|
121
|
+
to trust. It is not a sandbox against an agent that is trying to get around it. A
|
|
122
|
+
prompt-injected agent can still install through a variable (`P=npm; $P i x`), a script
|
|
123
|
+
file, or `python -c "import subprocess..."`. The command parser follows bash's quoting,
|
|
124
|
+
comments, heredocs and `$(...)` rules so ordinary commands can't hide an install by
|
|
125
|
+
accident, but no static check can see every way a shell can run code.
|
|
126
|
+
|
|
127
|
+
It is a heuristic guard, not a sandbox. It checks install commands and manifest edits made through the agent's edit tools. It does not check imports, and it does not see manifests changed by shell commands (`echo evil >> requirements.txt`). `pyproject.toml` and `Cargo.toml` edits are
|
|
128
|
+
only checked on Python 3.11+ (needs `tomllib`). Go modules on vanity domains (`go.uber.org/...`) get no star signal, and their age can be backdated. The pass cache is a convenience, not a security boundary: the hook refuses agent writes to it, but a determined agent could still reach it indirectly. Packages from custom indexes are
|
|
129
|
+
still checked against the public registry. Tarball and git URLs are not checked. Codex `apply_patch` patches are applied with a port of Codex's own matching rules; a patch Codex would reject is denied.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python3 -m unittest -v
|
|
135
|
+
python3 scripts/gen_popular.py # refresh the top-1000 npm/PyPI/crates lists in pkg_guard.py
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
MIT licensed.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# pkg-guard
|
|
2
|
+
|
|
3
|
+
AI coding agents sometimes install packages that don't exist. Attackers register
|
|
4
|
+
those hallucinated names, plus one-letter typos of popular packages, and wait
|
|
5
|
+
("slopsquatting"). **pkg-guard** is a hook that checks every install command your
|
|
6
|
+
agent runs and blocks the suspicious ones before they execute.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ pkg-guard check "pip install reqeusts"
|
|
10
|
+
BLOCK reqeusts: does not exist on pypi (hallucinated name?). Did you mean 'requests'?
|
|
11
|
+
|
|
12
|
+
$ pkg-guard check "npm i lodahs"
|
|
13
|
+
BLOCK lodahs: looks like a typosquat of popular package 'lodash'.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The agent sees the reason and fixes its own command.
|
|
17
|
+
|
|
18
|
+
- **Works with** Claude Code, Codex, and Gemini CLI. One script speaks all three hook protocols.
|
|
19
|
+
- **Covers** npm, pnpm, yarn, bun, npx, pip, uv, uvx, poetry, pipx, cargo, gem, and go (`go get`, `go install`, `go run mod@v`).
|
|
20
|
+
- **Single file, Python stdlib only.** The guard adds no dependencies of its own. Read it in five minutes.
|
|
21
|
+
|
|
22
|
+
## What gets blocked
|
|
23
|
+
|
|
24
|
+
| Signal | Rule |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Doesn't exist | Registry returns 404. Suggests the popular name it resembles. |
|
|
27
|
+
| Too new | First published less than 30 days ago (`PKG_GUARD_MIN_AGE_DAYS`) |
|
|
28
|
+
| Too few users | npm: fewer than 100 weekly downloads. crates: fewer than 100 in 90 days. gems: fewer than 1,000 total. Go (GitHub/GitLab/Bitbucket): fewer than 10 repo stars, since Go publish times come from git commits and can be backdated. |
|
|
29
|
+
| Typosquat | One edit away from a popular package, and young or little-used |
|
|
30
|
+
| Install script (npm) | Latest version runs `preinstall`/`install`/`postinstall` code, and the package is young (< 180 days) or little-used (< 10,000 weekly downloads) |
|
|
31
|
+
|
|
32
|
+
It also catches the usual ways around a naive regex: `bash -lc "..."`, `$(...)`,
|
|
33
|
+
`sudo`/`env` wrappers, `npm isntall`, `npm i x@npm:evil`, `npx -p evil`,
|
|
34
|
+
`uvx --with evil`, and `pip install -r requirements.txt` (nested `-r` included).
|
|
35
|
+
|
|
36
|
+
It also checks file edits (Claude Code `Write`/`Edit`, Codex `apply_patch`, Gemini CLI `write_file`/`replace`): dependencies the agent adds to
|
|
37
|
+
`package.json`, `requirements*.txt`, `pyproject.toml`, `Cargo.toml`, `Gemfile`, or `go.mod`
|
|
38
|
+
are checked before the edit is written, so a later bare `npm install` can't sneak them in.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
### Claude Code (plugin: hook + skill)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/plugin marketplace add somesh-ghaturle/pkg-guard
|
|
46
|
+
/plugin install pkg-guard@pkg-guard
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Codex
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/somesh-ghaturle/pkg-guard ~/.pkg-guard
|
|
53
|
+
mkdir -p ~/.codex/skills && cp -r ~/.pkg-guard/skills/pkg-guard ~/.codex/skills/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then add the hook to `~/.codex/config.toml`, or use [`examples/codex-hooks.json`](examples/codex-hooks.json) as `~/.codex/hooks.json`:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[[hooks.PreToolUse]]
|
|
60
|
+
matcher = "Bash|apply_patch"
|
|
61
|
+
|
|
62
|
+
[[hooks.PreToolUse.hooks]]
|
|
63
|
+
type = "command"
|
|
64
|
+
command = "python3 ~/.pkg-guard/pkg_guard.py hook"
|
|
65
|
+
timeout = 20
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Gemini CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/somesh-ghaturle/pkg-guard ~/.pkg-guard
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then merge [`examples/gemini-settings.json`](examples/gemini-settings.json) into `~/.gemini/settings.json`, with the command set to `python3 ~/.pkg-guard/pkg_guard.py hook`. It checks `run_shell_command` plus `write_file`/`replace` edits to manifests.
|
|
75
|
+
|
|
76
|
+
### Claude Code without the plugin
|
|
77
|
+
|
|
78
|
+
Add [`examples/claude-settings.json`](examples/claude-settings.json) to `~/.claude/settings.json`, using your own path to the script.
|
|
79
|
+
|
|
80
|
+
### As a CLI
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pipx install slopsquat-guard # the PyPI name; the command is pkg-guard
|
|
84
|
+
pkg-guard check "npm install some-lib" # exit 1 if blocked
|
|
85
|
+
pkg-guard scan # check every package in ./package-lock.json, uv.lock, ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The PyPI package is **`slopsquat-guard`**. `pkg-guard` on PyPI is a different, unrelated project.
|
|
89
|
+
|
|
90
|
+
`scan` reads `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock` (classic and Berry),
|
|
91
|
+
`pnpm-lock.yaml`, `uv.lock`, `poetry.lock`, `Cargo.lock` (the TOML ones need Python 3.11+),
|
|
92
|
+
`Gemfile.lock` and `go.sum`, skips git/path/workspace entries, and
|
|
93
|
+
applies the same rules to every registry package, transitive ones included. Use it once
|
|
94
|
+
when you add pkg-guard to an existing repo, or in CI.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
| Env var | Effect |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `PKG_GUARD_ALLOW=a,b` | Always allow these packages (internal or private names) |
|
|
101
|
+
| `PKG_GUARD_MIN_AGE_DAYS=30` | Minimum package age |
|
|
102
|
+
| `GOPRIVATE` / `GONOPROXY` | Go modules matching these globs are skipped, like `go` does (env vars only, not `go env -w`) |
|
|
103
|
+
| `XDG_CACHE_HOME` | Packages that pass are cached for 24h in `$XDG_CACHE_HOME/pkg-guard/pkg-guard-ok.json` (default `~/.cache`). Blocks are never cached. Delete the file to recheck everything. |
|
|
104
|
+
| `PKG_GUARD_FAIL_CLOSED=1` | Block when the registry can't be reached. The default is to allow and warn, so offline work keeps going. |
|
|
105
|
+
|
|
106
|
+
## Limits
|
|
107
|
+
|
|
108
|
+
pkg-guard catches agent **mistakes**: hallucinated names, typos, packages too new
|
|
109
|
+
to trust. It is not a sandbox against an agent that is trying to get around it. A
|
|
110
|
+
prompt-injected agent can still install through a variable (`P=npm; $P i x`), a script
|
|
111
|
+
file, or `python -c "import subprocess..."`. The command parser follows bash's quoting,
|
|
112
|
+
comments, heredocs and `$(...)` rules so ordinary commands can't hide an install by
|
|
113
|
+
accident, but no static check can see every way a shell can run code.
|
|
114
|
+
|
|
115
|
+
It is a heuristic guard, not a sandbox. It checks install commands and manifest edits made through the agent's edit tools. It does not check imports, and it does not see manifests changed by shell commands (`echo evil >> requirements.txt`). `pyproject.toml` and `Cargo.toml` edits are
|
|
116
|
+
only checked on Python 3.11+ (needs `tomllib`). Go modules on vanity domains (`go.uber.org/...`) get no star signal, and their age can be backdated. The pass cache is a convenience, not a security boundary: the hook refuses agent writes to it, but a determined agent could still reach it indirectly. Packages from custom indexes are
|
|
117
|
+
still checked against the public registry. Tarball and git URLs are not checked. Codex `apply_patch` patches are applied with a port of Codex's own matching rules; a patch Codex would reject is denied.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 -m unittest -v
|
|
123
|
+
python3 scripts/gen_popular.py # refresh the top-1000 npm/PyPI/crates lists in pkg_guard.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
MIT licensed.
|