Supply chain security plugin for Claude Code. Blocks compromised packages before they're installed.
attach-guard-demo.mp4
Claude Code installs packages on your behalf — often without you reviewing each one. Existing security tools scan after the fact or rely on advisory prompts that Claude can skip. There is no open-source guardrail that sits directly in front of package install commands and blocks risky packages before they execute.
attach-guard is a Claude Code plugin that intercepts package installation commands and evaluates them against policy before execution. It is not an advisory scanner. It is a hard enforcement boundary.
- Installs as a Claude Code plugin — no manual hook configuration needed
- Intercepts
npm install,pnpm add,pip install,go get, andcargo addcommands via PreToolUse hooks - Evaluates packages with the configured provider before install commands run
- Uses the current Socket.dev adapter only as an explicit bring-your-own-token local provider, not a hosted/default scoring source
- Denies known malware and high-confidence dangerous packages automatically
- Asks for confirmation on gray-band packages and provider-unavailable cases in local mode
- Rewrites unpinned installs to safe pinned versions when possible
- Logs every decision to a local JSONL audit trail
Attach Open Score is the first-party provider direction and is not wired in yet; see Attach Open Score provider semantics for the planned integration contract, Local Attach Open Score dogfood guide for public-safe local verification, and the dogfood plan for scope.
Most security tools just say "no." attach-guard says "no, but here's a safe alternative."
When a risky version is blocked, attach-guard finds the newest version that passes policy and offers it as a replacement. Claude sees the safe alternative and can proceed immediately — your flow doesn't stop, it gets redirected to a safe path.
npm — axios v1.14.1 and v0.30.4 were publicly reported compromised versions published via a hijacked maintainer account:
> npm install axios
attach-guard evaluates:
axios@1.14.1 --> DENY (known compromised version)
axios@1.14.0 --> ALLOW (passes configured policy checks)
Result: ASK + rewritten command
"npm install axios@1.14.0"
pip — litellm v1.82.7 and v1.82.8 were malicious versions published to PyPI:
> pip install litellm
attach-guard evaluates:
litellm==1.82.8 --> DENY (compromised version)
litellm==1.82.6 --> ALLOW (passes all policy checks)
Result: ASK + rewritten command
"pip install litellm==1.82.6"
These examples illustrate the current enforcement flow: attach-guard blocks known compromised or policy-failing versions and offers a safe pinned alternative when one is available.
| Scenario | Example | Decision | What happens |
|---|---|---|---|
| Package is safe | npm install axios@1.14.0 |
Allow | Install proceeds normally |
| Pinned to compromised version | pip install litellm==1.82.8 |
Deny | Blocked — compromised version |
| Unpinned, latest is risky | npm install axios |
Ask + rewrite | Safe alternative offered: axios@1.14.0 |
| All versions fail | malware-only package | Deny | Blocked with clear explanation |
This works across all supported ecosystems — the rewrite uses the native pinning syntax for each:
| Ecosystem | Unpinned command | Rewritten command |
|---|---|---|
| npm / pnpm | npm install axios |
npm install axios@1.14.0 |
| pip | pip install litellm |
pip install litellm==1.82.6 |
| Go | go get golang.org/x/net |
go get golang.org/x/net@v0.25.0 |
| Cargo | cargo add serde |
cargo add serde@=1.0.200 |
Your flow only fully stops when there is genuinely no safe version to offer.
attach-guard uses Claude Code hooks — not skills or MCP servers. The distinction matters:
- Hooks run automatically on every matching tool call. They enforce rules deterministically — Claude cannot skip or override them.
- Skills are instructions Claude follows when invoked. They guide behavior but cannot block actions.
- MCP servers provide advisory context. They inform but do not enforce.
Security enforcement requires interception at the tool-call boundary, before execution. Hooks are the only Claude Code extension point that guarantees this.
The fastest way to try the current packaged plugin. Today's released plugin uses the local bring-your-own-token Socket.dev provider for real scoring. Attach Open Score is the first-party direction and is not wired in yet; Socket must not be treated as hosted/default Attach scoring.
# Add the marketplace and install (one-time)
claude plugin marketplace add attach-dev/attach-guard
claude plugin install attach-guard@attach-devOr from within a Claude Code session:
/plugin marketplace add attach-dev/attach-guard
/plugin install attach-guard@attach-dev
During installation or enablement, the current Socket-backed plugin path prompts for your Socket API token (stored securely in your system keychain). Get a free token at socket.dev. This token is for local BYO-provider use only.
If the install/enable prompt didn't appear, re-trigger it with:
claude plugin disable attach-guard@attach-dev && claude plugin enable attach-guard@attach-devOr set the token as an environment variable:
export SOCKET_API_TOKEN="your-token"in your shell profile.
The prebuilt binary is downloaded automatically for your platform. The hook, config, and skill are all registered — no further setup needed.
Once running, the plugin provides:
- Automatic enforcement — direct
npm install,pnpm add,pip install,go get, andcargo addcommands are intercepted and checked /explain <package>— look up any package's risk score, alerts, and version history
If you want to develop or modify attach-guard, clone the repo and load the plugin directly. Requires Go 1.21+.
git clone https://github.com/attach-dev/attach-guard.git
cd attach-guard
claude --plugin-dir ./pluginThe binary auto-builds from source on the first /explain invocation.
Local claude --plugin-dir ./plugin development may not run the marketplace install/enable config flow. If Claude does not inject the plugin config in this mode, export SOCKET_API_TOKEN manually before starting Claude Code.
For use without the plugin system, or to install the binary globally.
- Go 1.21+ (to build from source; not needed for the plugin install above)
- A Socket.dev API token only when using the current Socket BYO-token provider path
make buildMove the binary somewhere on your PATH:
# Option A: Move to a standard location
sudo mv attach-guard /usr/local/bin/
# Option B: Move to a user-local bin directory
mkdir -p ~/.local/bin
mv attach-guard ~/.local/bin/
# Make sure ~/.local/bin is in your PATH (add to ~/.bashrc or ~/.zshrc):
# export PATH="$HOME/.local/bin:$PATH"Verify it works:
attach-guard version
# attach-guard v0.1.0export SOCKET_API_TOKEN="your-token-here"Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) to persist across sessions.
attach-guard config init
# Default config written to ~/.attach-guard/config.yamlThis creates ~/.attach-guard/config.yaml with sensible defaults. See Configuration below to customize policy thresholds.
Add the following to your project's .claude/settings.json (shared with team) or .claude/settings.local.json (personal, gitignored):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "attach-guard hook"
}
]
}
]
}
}For global protection across all projects, add it to ~/.claude/settings.json instead.
Try installing a known-compromised version to verify attach-guard blocks it:
> Install axios@1.14.1
Claude: I'll install axios@1.14.1.
[attach-guard] deny: axios@1.14.1: known compromised version
Then try a safe version:
> Install axios
Claude: I'll install axios.
[attach-guard] allow: package passes all policy checks
When Claude Code calls the Bash tool with a package install command (e.g., npm install axios, pip install requests, go get golang.org/x/net, cargo add serde):
- Claude Code fires the PreToolUse hook before execution
- The hook pipes the tool input JSON to
attach-guard hookvia stdin - attach-guard parses the command, evaluates packages against policy
- Returns a
hookSpecificOutputJSON response:permissionDecision: "allow"— install proceedspermissionDecision: "ask"— Claude shows the reason and asks the userpermissionDecision: "deny"— install is blocked, reason shown to Claude
- On internal errors, exits with code 2 (blocking) to fail closed
attach-guard evaluate <command> Evaluate a package manager command against policy
attach-guard hook [run] Read Claude Code hook JSON from stdin and respond
attach-guard run --dry-run <claude|codex> Preview a claude/codex command without executing it
attach-guard config init Write default config to ~/.attach-guard/config.yaml
attach-guard version Print version
attach-guard help Show help
# npm
attach-guard evaluate npm install axios
attach-guard evaluate npm install axios@1.14.1
# pip
attach-guard evaluate pip install litellm
attach-guard evaluate pip install litellm==1.82.8
# Go
attach-guard evaluate go get golang.org/x/net
attach-guard evaluate go get golang.org/x/net@v0.25.0
# Cargo
attach-guard evaluate cargo add serde
attach-guard evaluate cargo add serde@=1.0.200
# Use as a Claude Code hook (reads JSON from stdin)
attach-guard hook
# Preview supported agent commands without starting the agent
attach-guard run --dry-run claude
attach-guard run --dry-run codexDefault config location: ~/.attach-guard/config.yaml
Current provider configuration still defaults to the Socket adapter in code. Treat this as a legacy/local BYO-token default until the Attach Open Score provider lands; do not use Socket as hosted/default Attach scoring.
provider:
kind: socket # explicit BYO-token local provider today; Open Score is the first-party direction
api_token_env: SOCKET_API_TOKEN
policy:
deny_known_malware: true
min_supply_chain_score: 70 # hard allow threshold
min_overall_score: 70
gray_band_min_supply_chain_score: 50 # hard deny below this
minimum_package_age_hours: 48 # deny versions newer than this
provider_unavailable_behavior:
local: ask # ask | deny | allow
ci: deny
unknown_behavior:
local: ask # Open Score UNKNOWN: ask | deny | allow
ci: deny
auto_rewrite_unpinned:
local: false # auto-pin to safe version?
ci: false
allowlist: [] # always allow these packages
denylist: [] # always deny these packages
package_managers:
npm: true
pnpm: true
pip: true
go: true
cargo: true
logging:
path: "~/.attach-guard/audit.jsonl"ATTACH_GUARD_LOG_PATH— override log pathATTACH_GUARD_PROVIDER— override provider kind
Highest priority wins (later sources override earlier):
- Built-in defaults
- Plugin-bundled config (when installed as a plugin)
- User-global config (
~/.attach-guard/config.yaml) - Project-local config (
.attach-guard/config.yaml) - Environment variables
- Check allowlist/denylist
- Check provider availability
- Deny known malware
- Deny versions under minimum age (48 hours default)
- Use Attach Open Score-style provider verdicts when present (
ALLOW,ASK,DENY,UNKNOWN) - Deny scores below hard threshold (supply chain < 50) for legacy score providers
- Ask on gray-band scores (50-70)
- Ask on critical/high alerts
- Allow everything else
When you run an unpinned supported command such as npm install axios, pip install requests, go get golang.org/x/net, or cargo add serde:
- attach-guard fetches candidate versions from the configured provider and evaluates them against policy
- If the latest passes policy, the command runs as-is
- If the latest fails but an older version passes, attach-guard suggests a rewrite using ecosystem-native syntax
- In Claude Code mode: returns
askwith the rewritten command viaupdatedInput - If no version passes, denies
Attach Open Score integration should be verdict-first: ALLOW → allow, ASK → ask, DENY → deny, and UNKNOWN → ask/warn locally by default. CI/team policy may map unknowns to deny by explicit configuration. See Attach Open Score provider semantics.
- Local/interactive mode: asks on provider failure
- CI mode: denies on provider failure (fail closed)
- Internal errors in hook mode: exit code 2 (blocks the install)
Every decision is logged to ~/.attach-guard/audit.jsonl:
{
"timestamp": "2026-01-15T10:30:00Z",
"user": "dev",
"cwd": "/home/dev/project",
"package_manager": "npm",
"original_command": "npm install example-malware@1.0.0",
"decision": "deny",
"reason": "example-malware@1.0.0: known malware alert",
"packages": [{"ecosystem":"npm","name":"example-malware","selected_version":"1.0.0","score":{"supply_chain":0,"overall":0},"provider_verdict":{"decision":"DENY","risk_score":95,"reasons":["known-malware-synthetic"],"source_refs":["osv:GHSA-0000-0000-0000"]},"alerts":[{"severity":"critical","title":"known malware","category":"malware"}]}],
"provider": "mock",
"mode": "claude"
}The current Socket adapter uses the Socket.dev API when the user explicitly configures the BYO-token Socket provider. This section is for that local provider path only; Socket data must not be used as a default Attach Open Score input/source absent an explicit partnership.
| Ecosystem | Endpoint | Cost per call |
|---|---|---|
| npm / pnpm | GET /v0/npm/{name}/{version}/score |
10 units |
| PyPI, Go, Cargo | POST /v0/purl (batch) |
100 units |
What this means in practice:
- npm packages: ~50 individual version scores per hour
- PyPI/Go/Cargo packages: ~5 batch scoring calls per hour (each batch scores up to 10 versions)
- Pinned installs (e.g.
pip install litellm==1.82.8) use one call to score a single version - Unpinned installs (e.g.
pip install litellm) use one batch call to score up to 10 candidate versions
When quota is exhausted, provider calls fail. Current behavior depends on where the failure happens:
- Provider startup/unavailability follows
provider_unavailable_behavior; local defaults to ask/warn, and CI/team fail-closed behavior requires explicit configuration such asprovider_unavailable_behavior.ci: deny. - Pinned installs can still fail closed if a score request errors after the provider is considered available; do not present quota exhaustion as guaranteed local ask/warn for every path.
- Unpinned installs cannot offer a reliable safe-version rewrite because the rewrite feature requires real provider results to identify which version passes policy.
To check your remaining quota:
curl -s -H "Authorization: Bearer $SOCKET_API_TOKEN" "https://api.socket.dev/v0/quota"Quota resets hourly. For higher limits, see Socket.dev pricing.
- Direct
pip/pip3(includinguv pip),go get/go install, andcargo add/cargo installare supported;python -m pipremains passthrough for now - pip extras/range specs, Cargo requirement syntax, and non-semver Go queries are intentionally passed through for manual review rather than being auto-evaluated
- Current released provider implementation is Socket BYO-token/local; Attach Open Score first-party provider integration is the next direction
- PyPI, Go, and Cargo scoring through the Socket BYO provider uses Socket's
POST /v0/purlendpoint, which has higher quota cost (100 units) compared to npm (10 units) - No transitive dependency analysis
- No lockfile graph support
- Single provider at a time
- No org-level policy distribution
- No remote audit export
- Socket API response format may vary; adapter is based on documented endpoints
# Run all tests
make test
# Build
make build
# Evaluate a command
./attach-guard evaluate npm install lodashCross-compile plugin binaries for all platforms:
make plugin-buildTest the plugin locally:
claude --plugin-dir ./pluginMIT