Skip to content

Commit b72da78

Browse files
authored
Add dependency malware checker hook for Claude Code (#1184)
* feat(hooks): add check-new-deps hook to block malware packages Intercepts Edit/Write on dependency files across 17+ ecosystems (npm, PyPI, Cargo, Go, Maven, etc.) and checks new deps against Socket.dev's malware API before they're added. Uses SDK v4 checkMalware() with batch chunking, namespace-aware matching, and in-memory caching. * refactor(tests): use @socketsecurity/lib spawn and whichSync instead of child_process * fix(hooks): use JSON parser for Pipfile.lock instead of regex extractor Pipfile.lock is JSON (with "default" and "develop" sections keyed by package name), not requirements.txt format. The regex-based extractPypi silently matched zero dependencies. Add a dedicated extractPipfileLock that parses the JSON structure correctly. * fix(hooks): use nullish coalescing for new_string/old_string/content Empty string is a valid value for new_string (Edit that deletes content) and old_string. Using || instead of ?? caused falsy empty strings to fall through to the wrong field. * fix(hooks): add regex fallback for partial Pipfile.lock content * fix(hooks): strip .git suffix from Swift names, remove spurious brew key - Swift package URLs commonly end with .git (e.g. vapor.git); strip the suffix so the PURL lookup finds the correct package - Remove 'brew' extractor key that matched any path ending in 'brew'; only 'Brewfile' is the correct Homebrew manifest filename * fix: fix Cargo.toml and npm extractor false positives - Cargo.toml: Only extract deps from [dependencies] sections, not metadata like name, version, edition - npm: Exclude known package.json metadata field names that match the dep pattern but aren't dependencies
1 parent f49df6e commit b72da78

File tree

8 files changed

+1870
-0
lines changed

8 files changed

+1870
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# check-new-deps Hook
2+
3+
A Claude Code pre-tool hook that checks new dependencies against [Socket.dev](https://socket.dev) before they're added to the project. It runs automatically every time Claude tries to edit or create a dependency manifest file.
4+
5+
## What it does
6+
7+
When Claude edits a file like `package.json`, `requirements.txt`, `Cargo.toml`, or any of 17+ supported ecosystems, this hook:
8+
9+
1. **Detects the file type** and extracts dependency names from the content
10+
2. **Diffs against the old content** (for edits) so only *newly added* deps are checked
11+
3. **Queries the Socket.dev API** to check for malware and critical security alerts
12+
4. **Blocks the edit** (exit code 2) if malware or critical alerts are found
13+
5. **Warns** (but allows) if a package has a low quality score
14+
6. **Allows** (exit code 0) if everything is clean or the file isn't a manifest
15+
16+
## How it works
17+
18+
```
19+
Claude wants to edit package.json
20+
21+
22+
Hook receives the edit via stdin (JSON)
23+
24+
25+
Extract new deps from new_string
26+
Diff against old_string (if Edit)
27+
28+
29+
Build Package URLs (PURLs) for each dep
30+
31+
32+
Call sdk.checkMalware(components)
33+
- ≤5 deps: parallel firewall API (fast, full data)
34+
- >5 deps: batch PURL API (efficient)
35+
36+
├── Malware/critical alert → EXIT 2 (blocked)
37+
├── Low score → warn, EXIT 0 (allowed)
38+
└── Clean → EXIT 0 (allowed)
39+
```
40+
41+
## Supported ecosystems
42+
43+
| File | Ecosystem | Example dep format |
44+
|------|-----------|-------------------|
45+
| `package.json` | npm | `"express": "^4.19"` |
46+
| `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` | npm | lockfile entries |
47+
| `requirements.txt`, `pyproject.toml`, `setup.py` | PyPI | `flask>=3.0` |
48+
| `Cargo.toml`, `Cargo.lock` | Cargo (Rust) | `serde = "1.0"` |
49+
| `go.mod`, `go.sum` | Go | `github.com/gin-gonic/gin v1.9` |
50+
| `Gemfile`, `Gemfile.lock` | RubyGems | `gem 'rails'` |
51+
| `composer.json`, `composer.lock` | Composer (PHP) | `"vendor/package": "^3.0"` |
52+
| `pom.xml`, `build.gradle` | Maven (Java) | `<artifactId>commons</artifactId>` |
53+
| `pubspec.yaml`, `pubspec.lock` | Pub (Dart) | `flutter_bloc: ^8.1` |
54+
| `.csproj` | NuGet (.NET) | `<PackageReference Include="..."/>` |
55+
| `mix.exs` | Hex (Elixir) | `{:phoenix, "~> 1.7"}` |
56+
| `Package.swift` | Swift PM | `.package(url: "...", from: "4.0")` |
57+
| `*.tf` | Terraform | `source = "hashicorp/aws"` |
58+
| `Brewfile` | Homebrew | `brew "git"` |
59+
| `conanfile.*` | Conan (C/C++) | `boost/1.83.0` |
60+
| `flake.nix` | Nix | `github:owner/repo` |
61+
| `.github/workflows/*.yml` | GitHub Actions | `uses: owner/repo@ref` |
62+
63+
## Configuration
64+
65+
The hook is registered in `.claude/settings.json`:
66+
67+
```json
68+
{
69+
"hooks": {
70+
"PreToolUse": [
71+
{
72+
"matcher": "Edit|Write",
73+
"hooks": [
74+
{
75+
"type": "command",
76+
"command": "node .claude/hooks/check-new-deps/index.mts"
77+
}
78+
]
79+
}
80+
]
81+
}
82+
}
83+
```
84+
85+
## Dependencies
86+
87+
All dependencies use `catalog:` references from the workspace root (`pnpm-workspace.yaml`):
88+
89+
- `@socketsecurity/sdk` — Socket.dev SDK v4 with `checkMalware()` API
90+
- `@socketsecurity/lib` — shared constants and path utilities
91+
- `@socketregistry/packageurl-js` — Package URL (PURL) parsing and stringification
92+
93+
## Caching
94+
95+
API responses are cached in-memory for 5 minutes (max 500 entries) to avoid redundant network calls when Claude checks the same dependency multiple times in a session.
96+
97+
## Exit codes
98+
99+
| Code | Meaning | Claude behavior |
100+
|------|---------|----------------|
101+
| 0 | Allow | Edit/Write proceeds normally |
102+
| 2 | Block | Edit/Write is rejected, Claude sees the error message |

0 commit comments

Comments
 (0)