JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1464
  • Score
    100M100P100Q93014F
  • License MIT

Supply-chain firewall for AI coding tools

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 publish

    The 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.md

    What 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 init

    From 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 normally

    The 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 version

    How the API key flows

    Priority order (first non-empty value wins):

    1. EXPECTO_API_KEY environment variable
    2. ~/.expecto/api_key file (written by expecto login / expecto init)
    3. OS keychain (read by keychain.py; also written on expecto 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 whoami

    Step 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 publish

    That'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