Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (expecto-security) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
expecto-security
Supply-chain firewall for AI coding tools.
One command installs a real-time package security gate inside Claude Code, Cursor,
and every terminal on a developer's machine. Before any npm install or pip install
runs — whether typed by a human or generated by an AI agent — Expecto checks the
package against a global verdict cache built from real sandbox detonations.
npx expecto-security init # Node.js path — zero install, works anywhere
pipx install expecto-security # Python path — after PyPI publishThe CLI command after install is always expecto.
What was built
apps/cli/ is a Python/Typer package published to npm as expecto-security.
The npm package (bin/expecto.js) is a thin Node.js shim that detects Python,
installs the Python package if needed, then delegates every command to it.
This means one npx expecto-security init bootstraps everything regardless of
whether the developer thinks in npm or pip terms.
Repository layout
apps/cli/
├── pyproject.toml ← Python package: expecto-security
├── package.json ← npm package: expecto-security
├── bin/
│ └── expecto.js ← npm shim (detects Python, installs, delegates)
└── expecto/
├── __init__.py ← version string
├── __main__.py ← python -m expecto entry
├── main.py ← Typer app, registers all subcommands
├── keychain.py ← OS keychain + ~/.expecto/api_key fallback
├── shim.py ← writes/removes ~/.expecto/bin/* shim scripts
├── commands/
│ ├── init.py ← writes all five project artifacts
│ ├── login.py ← set / update API key
│ ├── check.py ← one-off manual package check
│ └── update.py ← re-writes hook to latest bundled version
└── templates/
├── pre_tool_use_hook.py ← Claude Code PreToolUse hook (source of truth)
├── mcp_server.py ← stdio MCP bridge → proxies to REST /check
├── cursor_rule.md ← content written to .cursor/rules
└── claude_md_block.md ← content appended to CLAUDE.mdWhat expecto init writes
Run once per project. Every write is idempotent — re-running only updates what changed, never clobbers unrelated config.
In the project directory
| File | What it does |
|---|---|
.claude/hooks/pre_tool_use.py |
The security gate. Claude Code runs this before every Bash tool call. Parses the command, extracts package names, calls /check for each, blocks with reason + alternative on BLOCK. |
.claude/settings.json |
Registers the hook with Claude Code's PreToolUse system. Merges with any existing settings — never overwrites keys it doesn't own. |
.mcp.json |
Registers the Expecto MCP server so Claude Code and Cursor agents can call check_package as an explicit MCP tool (in addition to the hook's passive interception). |
.cursor/rules |
Cursor rule with alwaysApply: true — instructs the agent to call check_package before suggesting any install and to explain the alternative on BLOCK. |
CLAUDE.md |
Context block appended to the project CLAUDE.md. Tells Claude Code to prefer packages with clean verdicts and never install without checking. |
In the user's home directory (once, shared across all projects)
| File | What it does |
|---|---|
~/.expecto/api_key |
API key stored at mode 0600. The hook reads this as a fallback when EXPECTO_API_KEY is not in the environment. |
~/.expecto/mcp_server.py |
Stdio JSON-RPC server. Claude Code and Cursor spawn it as a subprocess when they want to call check_package as a tool. Proxies to the REST /check endpoint. |
~/.expecto/bin/{npm,pip,pip3,yarn,pnpm} |
Python wrapper scripts. When ~/.expecto/bin is early in PATH, every terminal install goes through the same check before the real package manager runs. |
What you can do right now
Protect a project
cd any-project
npx expecto-security initFrom that moment, every npm install or pip install the AI agent (or you) runs
in that project is checked before execution.
The three interception paths
Path 1 — Claude Code hook (passive)
Claude Code fires .claude/hooks/pre_tool_use.py before every Bash tool call.
The hook parses the command string, finds every package being installed (including
manifest-driven installs from package.json or requirements.txt), calls
POST /check for each one, and either allows the install silently or blocks it
with a formatted deny message that Claude Code shows inline.
🧙 Expecto — 🚨 Package install blocked due to security threat!
❌ event-stream@4.0.0
🔍 reason: post-install script exfiltrates ~/.npm/credentials via R2b
📋 rules: R2b, R1
✅ safe alternative: readable-stream@4.1.0
🛡️ Expecto blocked this install. Ask me to find a safe alternative.Path 2 — MCP tool (active, agent-native)
When .mcp.json is present, Claude Code and Cursor can call check_package
as an explicit tool before they even write the install command. The Cursor rule
and CLAUDE.md block tell agents to do this proactively. The MCP bridge at
~/.expecto/mcp_server.py handles the protocol translation.
Path 3 — Shell shims (terminal)
With ~/.expecto/bin first in PATH, every terminal install is intercepted:
npm install event-stream # shim fires → BLOCK printed → npm never runs
pip install requests # shim fires → ALLOW → pip runs normallyThe shim finds the real binary by rebuilding PATH without ~/.expecto/bin,
then execs into it if the check passes.
Commands
expecto init # initialize in current project
expecto init --yes --no-shims # non-interactive, skip shims
expecto init --mcp-url http://localhost:8000 # point at local dev server
expecto login # prompt for API key, save to keychain
expecto login sk_live_abc123 # set key non-interactively
expecto check lodash # check npm package
expecto check requests --ecosystem pypi # check PyPI package
expecto check express --version 4.18.2 # check specific version
expecto update # re-write hook to latest bundled versionHow the API key flows
Priority order (first non-empty value wins):
EXPECTO_API_KEYenvironment variable~/.expecto/api_keyfile (written byexpecto login/expecto init)- OS keychain (read by
keychain.py; also written onexpecto login)
This means the key works in Claude Code, in terminal shims, and in CI
(EXPECTO_API_KEY env var) without any extra setup per context.
Packages intercepted
The hook and shims parse and intercept all of these:
| Command form | Ecosystem |
|---|---|
npm install <pkg>, npm i, npm add, npm ci |
npm |
npx <pkg> |
npm |
yarn add <pkg>, yarn install |
npm |
pnpm add <pkg>, pnpm install |
npm |
pip install <pkg>, pip3 install |
PyPI |
python -m pip install, python3 -m pip install |
PyPI |
uv pip install |
PyPI |
pipx install |
PyPI |
npm install (no args — reads package.json) |
npm |
pip install -r requirements.txt (reads the file) |
PyPI |
How to publish updates
Step 1 — log into npm (one-time)
npm login
# Follow the prompts. Uses your npmjs.com account.
# Verify: npm whoamiStep 2 — make your changes
Edit any files under apps/cli/expecto/. If you changed the hook logic,
update the template first:
apps/cli/expecto/templates/pre_tool_use_hook.py ← source of truth
.claude/hooks/pre_tool_use.py ← dev copy (keep in sync)Step 3 — bump the version
All three files must match:
# apps/cli/pyproject.toml → version = "0.1.1"
# apps/cli/package.json → "version": "0.1.1"
# apps/cli/expecto/__init__.py → __version__ = "0.1.1"Step 4 — publish npm
cd apps/cli
npm publishThat's it for the npm package. The npm package (bin/expecto.js) auto-installs
the Python package from PyPI on first run, so npm is the primary distribution channel.
Step 5 — publish PyPI (optional, for pipx install expecto-security)
pip3 install build twine --break-system-packages
cd apps/cli
python3 -m build
twine upload dist/*Step 6 — tell existing users to update their hooks
Users who already ran expecto init have the old hook. They update with:
npx expecto-security@latest update # or: expecto update (if already installed)What's wired vs. what needs server work
| Feature | Works today | Needs |
|---|---|---|
| Claude Code hook interception | ✅ | Nothing — points at any /check endpoint |
| Shell shims | ✅ | PATH update in shell rc |
| MCP server registration | ✅ | File is written; server spawns correctly |
expecto check CLI |
✅ | Live MCP gate at EXPECTO_MCP_URL |
| Cursor rules injection | ✅ | Cursor restart |
| CLAUDE.md context block | ✅ | Immediate |
alternative shown on BLOCK |
✅ (renders it) | A4 to populate the field server-side |
code --install-extension gate |
⏳ | One elif branch in the hook + B7 server work |
| PyPI publish | ⏳ | twine upload after python3 -m build |
| npm publish | ⏳ | npm login then npm publish |