Zn Gate README

The full upstream README, mirrored for reference. Install config, tool schemas, adoption signals and our own overview live on the Zn Gate listing page.

zn

zn-gate

The local security layer for coding agents.
zn-gate guards your secrets, your data and your systems while you use Claude Code, Codex CLI, Cursor, OpenCode, Antigravity or ZCode desktop app.
An open-source security layer that runs on your machine, between your agent and the tools and content it touches.

CI npm version PyPI version License: MIT

Docs · npm package · Python package · Security · Discord


For: developers using Claude Code, Codex CLI, Cursor, OpenCode, Antigravity, ZCode desktop app or any MCP client, and teams building agents with MCP, LangChain, CrewAI or LlamaIndex.

What it does: keeps secret values out of what your agent sends to its model, checks what your agent reads and calls for prompt injection, and audits prompts, skills and configs. Everything runs on your machine, is MIT-licensed and needs no account. The Python package has no runtime dependencies; the npm package's only one is age-encryption (with its @noble/@scure dependencies) for the secret vault, bundled in the package.

Install

Everything runs on your machine; no account or API key is needed. Node.js 22 LTS is recommended (from nodejs.org). Node.js 18 or later works for the hooks and the CLI; the Claude Code plugin needs 20.19 or later. For security, use the latest patch of a supported Node.js line (22.23.4, 24.22.1, 26.12.1 or later): Node.js 18 and 20 are end-of-life and get no security fixes, even though zn-gate still runs on them. With a Node.js older than 18, the zn-gate and zn commands stop and say how to install it.

Getting started: four steps, one command each. Run them in your own terminal.

  1. npx -y zn-gate reach shows what your coding agents can reach on this machine: secrets in your .env files (by name only), credential files and MCP servers. It is read only and sends nothing. Details in docs/reach.md.
  2. npx -y zn-gate init sets up protection for the coding agents it finds, one step at a time, and asks before each change.
  3. npx -y zn-gate check tests that protection with a fake secret in a temporary folder. init runs it at the end too.
  4. npx -y zn-gate env shadow .env moves the real values of your .env into the local vault and leaves markers in the file. The first time, run npx -y zn-gate unlock to create the vault.

One command for the coding agents on your machine:

Terminal
npx -y zn-gate init

init does this:

  • It finds Claude Code, Codex CLI, Cursor, OpenCode, Antigravity and ZCode desktop app on your machine.
  • For each one that is not protected yet, it shows the setup: the zn-gate plugin for Claude Code (or zn's hooks when your Node.js is older than 20.19), and zn's hooks for the others.
  • It runs each step only after you confirm it.
  • It ends with zn-gate check.

Run it yourself, in your own terminal, with no arguments. When an AI agent or a script runs it, it only prints the steps.

Windows PowerShell: npm and npx can fail with "running scripts is disabled on this system", because the default execution policy blocks npm.ps1. Use npm.cmd, npx.cmd and zn.cmd instead (for example npm.cmd install -g zn-gate), run the commands from Command Prompt, or allow local scripts for your account with Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.

If something fails:

  • npx: command not found, or a message about the Node.js version: install the LTS version of Node.js, open a new terminal and run the command again.
  • "running scripts is disabled on this system" on Windows: see the PowerShell note above.
  • init only prints steps and changes nothing: an AI agent or a script ran it. Run it yourself in your own terminal.
  • check shows NOT protected: that line says why and what to run. What the output means.
  • You want to remove zn-gate: npx -y zn-gate uninstall shows what it will remove (the Claude Code plugin, zn's hooks, wrapped MCP servers) and asks first. init --revert only puts back MCP configs saved before wrapping.

Then keep your .env values in the vault. With the plugin or the hooks, the agent cannot read .env files with real values; move them into zn-gate's encrypted vault, and the file keeps markers such as __ZN_STRIPE_KEY__:

Terminal
npx -y zn-gate unlock            # creates the vault the first time
npx -y zn-gate env shadow .env

After this, your own app also reads the markers in .env: start it with zn-gate's run command once you allow it (docs/secrets.md). env shadow tells you how to undo it.

New here? First 5 minutes. All the guides: docs index. Five-minute walkthrough: docs/secrets.md. Limits: docs/threat-model.md. Words you do not know: docs/glossary.md. Every command and exit code: docs/cli.md. Removing zn-gate: docs/uninstall.md.

Other ways to install: one agent at a time, other editors, Python, MCP server only, wrapping the MCP servers you already use

One agent at a time (the same steps init runs):

Claude Code: the plugin. You also need Git and Claude Code 2.1.295 or later. The plugin needs Node.js 20.19 or later. With an older Node.js, npx -y zn-gate init skips the plugin and sets up zn's hooks for Claude Code instead (zn-gate check says so if the plugin is installed); update Node.js to use the plugin.

Terminal
claude plugin marketplace add usezn/zn
claude plugin install zn-gate@usezn

Already ran zn hooks install claude-code before? Run zn hooks uninstall claude-code first, otherwise both sets of hooks run.

Start a new Claude Code session. Claude Code is then blocked from reading .env files with real values and credential files, secrets in what its tools return are replaced with markers or redacted before the model sees them, a secret pasted into a prompt is blocked. The plugin adds no MCP server since version 1.14.0: for the secrets server, run zn hooks install claude-code instead of the plugin. Not covered: Claude Code sessions in the cloud, Cowork and chat. Details: plugins/zn-gate.

Codex CLI, Cursor or OpenCode: the hooks.

Terminal
npm install -g zn-gate
zn hooks install codex           # or: cursor, opencode

In Codex, approve the new hooks with /hooks. In Cursor, shell output and file contents are not scrubbed. What each agent supports: docs/compat.md.

Other editors (VS Code, Windsurf, Cline, Zed, JetBrains, Continue): zn-gate works there as an MCP server only. Setup for each one: usezn.com/docs/integrations/editors.

Antigravity. zn hooks install antigravity (or npx -y zn-gate init) adds a hook that runs before each tool call. It blocks reads and writes of secret files and shell commands that handle secrets, and the agent cannot edit the hook files. It cannot remove secrets from what a tool returns, Antigravity's MCP configuration files are not protected, and Antigravity does not document what happens when a hook fails (in our tests the call is blocked), so treat it as a first layer. The MCP server works there too. Details: Antigravity page.

ZCode desktop app. zn hooks install zcode (or npx -y zn-gate init) adds hooks that run before each tool call and before each prompt. It blocks reads and writes of secret files, shell commands that handle secrets, MCP calls that would send a secret, and a prompt that contains a secret, and the agent cannot edit ZCode's own config file. It cannot remove secrets from what a tool returns (zn adds a notice when an output holds one), ZCode ignores the hooks of a project, and zn's launcher blocks the call when its Node.js is gone, so treat it as a first layer. ZCode's documentation does not say what happens when a hook fails; in our tests with the ZCode desktop app 3.16.3 on Linux, a hook that failed (an exit code other than 2, a crash or a timeout) let the call run, and only exit code 2 or a deny answer blocked it. Tested end to end on the ZCode desktop app 3.16.3 on Linux x64 and Windows x64 (Windows Server 2022, through the agent the app ships; the Windows app window was not driven); macOS is not tested. Details: ZCode page.

Copilot CLI (experimental). zn hooks install copilot adds hooks that run before and after each tool call. They block reads and writes of secret files and shell commands that handle secrets, and replace secret values in the results of read-type and shell tools. They cannot block a typed prompt, Copilot lets a call through when a hook times out, so zn, which sets a 15-second timeout on its hooks, blocks the call itself at 11 seconds, it has not been tested on Windows, and Copilot's own session log keeps the original output, so treat it as a first layer. Details: Copilot CLI notes.

Gemini CLI (experimental). zn hooks install gemini adds a hook that runs before each tool call. It blocks reads and writes of secret files and shell commands that handle secrets, and the agent cannot edit Gemini CLI's settings files. It does not change what a tool returns, Gemini CLI hooks cannot ask you for confirmation, and Gemini CLI lets a call run when a hook crashes or times out, so zn blocks it instead (a hook that fails is answered with a block, and zn sets a 15-second timeout on its hook and answers a check still running after 11 seconds with a block), so treat it as a first layer. Google replaced Gemini CLI with Antigravity CLI for free and Google One accounts on 18 June 2026; Gemini CLI keeps working with zn-gate. Details: Gemini CLI notes.

Wrap any MCP server with the shield proxy, try it in shadow mode first, and review changes to its tools with pins: docs/shield.md.

Python: for agents you build yourself (rules engine and secret redaction for LangChain, CrewAI, LlamaIndex or plain tool calling). It does not protect Claude Code, Codex, Cursor or other coding agents. Install it in a virtual environment; many recent Linux systems, and Homebrew's Python on macOS, refuse pip install outside one ("externally-managed-environment"):

On Debian and Ubuntu, python3 -m venv needs the python3-venv package first: sudo apt install python3-venv.

bash
python3 -m venv .venv
.venv/bin/pip install zn-gate      # Windows: .venv\Scripts\pip install zn-gate

Only the command-line tools (scan, test, status)? Install the Python CLI as a standalone tool, without a virtual environment:

bash
pipx install zn-gate
pipx ensurepath        # then close and reopen your terminal
zn-gate scan .

pipx ensurepath puts ~/.local/bin on your PATH. Until you reopen the terminal, zn-gate is reported as "command not found". With uv: uv tool install zn-gate (run uv tool update-shell if the command is not found). To run it once without installing: uvx zn-gate scan .

The Python CLI does not install hooks. To protect Claude Code, Codex CLI, Cursor, OpenCode, Antigravity or ZCode desktop app, run npx -y zn-gate init (Node 18 or later).

For LangChain and LangGraph, ZnGuardCallbackHandler stops the run when it blocks a prompt, a tool call or a tool output, and guard_langchain_tool(tool) wraps a tool so its arguments are checked before it runs and its output is checked and masked before the model reads it. A callback cannot change what a tool returns, so the handler with mask_secrets=True (default) also stops the run on a secret in a tool output (it fails closed); to hide secrets and keep going, wrap the tool and create the handler with mask_secrets=False. See the Python package guide.

zn-gate as an MCP server only (the agent calls it when it decides to; not automatic protection):

Terminal
claude mcp add zn-gate -- npx -y zn-gate mcp     # Claude Code
codex mcp add zn-gate -- npx -y zn-gate mcp      # Codex CLI

Cursor (~/.cursor/mcp.json) and other editors: { "mcpServers": { "zn-gate": { "command": "npx", "args": ["-y", "zn-gate", "mcp"] } } }. OpenCode (~/.config/opencode/opencode.json): { "mcp": { "zn-gate": { "type": "local", "command": ["npx", "-y", "zn-gate", "mcp"], "enabled": true } } }.

Protect the MCP servers you already use. init finds them in your agent configs, writes a backup, and wraps each local server with the zn-gate proxy:

Terminal
npx -y zn-gate init --dry-run   # preview
npx -y zn-gate init --wrap-mcp  # wrap (backup first)
npx -y zn-gate init --revert    # put back the MCP configs saved before wrapping

It reads Claude Desktop, Claude Code (~/.claude.json), Cursor (~/.cursor/mcp.json), Codex CLI (~/.codex/config.toml), OpenCode (~/.config/opencode/opencode.json) and Antigravity (~/.gemini/antigravity/mcp_config.json). Full list and rules.

See what zn-gate did

Terminal
npx -y zn-gate report

A short summary of the last 7 days on this computer: blocked actions, secrets hidden, new protected MCP servers and tool changes waiting for your approval. It reads only local files, needs no account and sends nothing. --since 30d changes the period, --json prints it for scripts. What it shows.

Turn on the optional cloud check

Terminal
npx -y zn-gate connect

Optional, and you do it yourself in your own terminal (an agent cannot). It opens usezn.com so you can create an analysis-only API key (it cannot read your usage or manage keys), then asks you to paste it at a hidden prompt. A full-access key is refused. The key is kept in your system keychain (or the zn vault when there is no keychain), never written to a file, and zn's hooks refuse the direct ways for your agent to read it. After that, the text you or your agent send through analyze_prompt, with detected secrets removed first, is also checked by the Galvanize 2 classifier at usezn.com. Until you run it, nothing is sent. zn-gate disconnect removes the key from this computer and asks whether to revoke it on usezn.com too (default no). How it works.

Check that it works

Terminal
npx -y zn-gate check

For each agent set up for your user, zn-gate asks its hook to let the agent read a .env file with a fake key and an ordinary file, in a temporary folder. It shows what your agent would be told, and a line that says NOT protected says how to fix it. Nothing is sent over the network and no setting changes. What the output means.

Terminal
npx -y zn-gate test

This runs the local self-test (51 curated vectors: 26 attacks, 25 benign) and shows per-decision latency on your machine. It is a smoke test, not a detection rate. Nothing is sent over the network.

For developers: see the redaction by hand. Send a tool output with a fake AWS key (AWS's documentation example) and a database password through zn-gate, exactly as your agent would, and look for sanitized_content in the output; that field is what the model would see. In the full output, verdict: allow only means that no prompt injection was found; the secrets are hidden in sanitized_content (here [REDACTED_AWS_KEY] and [REDACTED_DB_PASSWORD]), and secrets_redacted counts them:

bash
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_tool_result","arguments":{"tool_name":"read_file","content":"AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nDATABASE_URL=postgres://app:hunter2@db:5432/main"}}}' \
  | npx -y zn-gate@latest mcp

Python equivalent:

server.ts
.venv/bin/python -c "from zn_gate import sanitize_tool_result as s; print(s('read_file', 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE')['sanitized_content'])"

What each piece does

Where zn-gate sits in a tool call, as an MCP server or as the shield proxy around other servers:

Sequence diagram. The agent sends analyze_prompt to zn-gate on your machine, which allows or blocks it. Then check_tool_call, which allows the call with secrets masked or blocks it. The agent runs the tool. The output comes back through check_tool_result or shield, and zn-gate returns it with secrets redacted, or neutralized if it carries an injection.

Scrubber: secrets stay out of the model's context

  • Today, through zn-gate: redacts AWS keys, OpenAI and Anthropic keys, GitHub tokens, JWTs, private keys and database URL passwords in everything that passes through zn-gate: tool output checked with check_tool_result, tool arguments checked with check_tool_call, and output of MCP servers wrapped by zn-gate shield / init. Python: sanitize_tool_result, @guard(mask_secrets=True) and the framework integrations.
  • Today, agent hooks and vault (npm package): hooks for Claude Code, Codex CLI and OpenCode scrub the output of the agents' built-in tools and block pasted secrets (in Cursor only MCP tool output is scrubbed); a local encrypted vault with stable markers (__ZN_AWS_KEY_1__); zn run gives a value (as an environment variable) only to commands you approved; personal data (cards, IBAN, national IDs, e-mail, phone) is detected too. What each agent allows differs: see docs/compat.md.

Galvanize: prompt-injection detection

  • Today, local: a deterministic rules engine (signatures plus normalization against homoglyphs, zero-width characters, BiDi overrides, comment splicing and Base64) checks prompts, tool arguments, tool output and MCP tool descriptions.
  • Today, optional cloud check: the Galvanize 2 classifier (open weights on Hugging Face, Apache-2.0) can also check prompts sent to analyze_prompt, only if you set ZN_CLOUD=1 and ZN_API_KEY. The secrets zn-gate detects are removed from the prompt on your machine before it is sent; personal data is not removed on this path, and a secret zn-gate does not recognize is sent with the text.
  • Roadmap: running Galvanize locally inside zn-gate.

Scan: audit what your agent is about to trust

  • Today: zn-gate scan <path> audits prompt templates, AGENTS.md / SKILL.md files and configs for injection traps and leaked secrets (exit code 1 on findings, for CI). A link given as the folder to scan is followed. When no file is scanned, the report says "No files were scanned in " and the exit code is 3. A file or folder that could not be read (for example a file of 2 MiB or larger) is listed and also makes the exit code 3, unless you pass --allow-partial. A leaked secret is reported as Secret found, with the next step: npx -y zn-gate env shadow <file> moves the values of a .env file into the local vault, npx -y zn-gate secret set NAME stores any other one. It finds provider keys and tokens, opaque Authorization: Bearer tokens, passwords in URLs, and secrets passed as command-line flags (--token=...) or assigned to a name such as password or api_key. The same check runs as a GitHub Action. It also checks the MCP server configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, .codex/config.toml, claude_desktop_config.json and others) for keys and passwords written in plain text in env, headers, arguments or URLs; it never prints the value, and --include-home checks your own MCP configs too. MCP servers can be checked on the web at usezn.com/scan.
  • Roadmap: auditing MCP servers and skills from the CLI before you add them.

What the cloud adds for teams

Everything above runs locally, for free, with no account. The optional cloud at usezn.com is for teams that need a shared record of what their agents did:

  • Audit and traceability: an evidence dashboard with the metadata of each cloud decision (verdict, rule, score, latency), 90-day retention and CSV/JSONL export.
  • A larger detection model for prompts, when you opt in (ZN_CLOUD=1).
  • Team controls (shared policies, organization-level audit): in progress.

By default nothing is sent. If you set ZN_CLOUD=1 and an API key, run zn-gate connect, or use the API key option of the Claude Code plugin, analyze sends the prompt text after removing the secrets zn-gate detects locally: personal data is not removed on this path, and a secret it does not detect would be sent with the text. The dashboard keeps the metadata of each cloud decision. Tool output and tool-call arguments are never sent. If the cloud call fails (no connection, a timeout, an error from the service, or a saved key that cannot be read), the prompt is checked by the local rules alone and the result says fallback: true. The check does not fail closed: a prompt the local rules allow goes through.

What leaves your machine

Nothing, by default.

Flow diagram. The agent, zn-gate and the tools or MCP servers run on your machine, and tool calls and prompts go through zn-gate. Tool output comes back redacted. Nothing leaves the machine by default. Only with ZN_CLOUD=1 and ZN_API_KEY set (or the Claude Code plugin's API key option), prompts with detected secrets removed go to api.usezn.com, the Galvanize 2 classifier.

DataEvaluatedSent anywhere?
Tool output (check_tool_result, shield)LocallyNever, in any configuration
Tool-call arguments (check_tool_call, shield)LocallyNever, in any configuration
MCP tool and parameter descriptions (shield)LocallyNever, in any configuration
Counts of what zn-gate did (zn-gate stats: checks, blocks, secrets replaced, per secret type)Written locally to ~/.zn/stats.jsonNever, unless you turn on zn-gate stats share on: then, each time you run zn-gate stats, one report per complete day (at most the last 3 days) with a random installation id, the day, the zn-gate, OS and Node.js versions, the agent and part of zn-gate used most, and the number of secrets found per built-in type (never values, prompts, variable or secret names, paths, hosts or emails). zn-gate stats share show shows it first.
Prompts (analyze_prompt, zn-gate analyze, Python analyze())LocallyOnly if you set both ZN_CLOUD=1 and ZN_API_KEY, run zn-gate connect yourself (Node package), or use the API key option of the Claude Code plugin (for analyze_prompt only). Detected secrets are removed locally before sending. Personal data, and any secret zn-gate does not recognize, is sent as written.
Team audit trail (~/.zn/audit.jsonl)Written locallyOnly on a machine joined to a Team organization with zn team join: while the vault is unlocked, the vault daemon syncs a few seconds after zn unlock and then every 15 minutes, and uploads audit metadata only (time, secret name, how it was used, agent, tool, command name, host, decision and its reason, written since you joined; earlier records only with zn team join --include-history) to your organization. Never secret values, prompts, file contents or tool output. See zn Team.
Secret values in the vault (secrets_http_request, zn run)Encrypted locallyNever to zn. zn gives a value only to a program you allowed (zn run, in its environment) or puts it into a request the agent asks for, and only to hosts you allowed for that secret (headers only by default). What an allowed program does with it is up to that program (limits).
zn-gate connect and zn-gate disconnectLocallyOnly when you run them yourself in your own terminal. They send only the API key, to api.usezn.com: to check it (/prod/usage) and, with disconnect --revoke or when you say yes, to revoke it (/prod/keys/self).

ZN_API_KEY alone enables nothing. zn-gate connect is the other way to opt in: you run it, in your own terminal, and it keeps the key in your system keychain or the zn vault, never in a file (zn-gate disconnect undoes it and can revoke the key on usezn.com when you say yes; details). ZN_LOCAL_ONLY=true overrides ZN_CLOUD=1 and a connected key. npx -y zn-gate status shows the active detection engine and whether the optional cloud check is on. Sharing of usage statistics is off by default and has its own status: npx -y zn-gate stats share status.

1.4.0 and earlier: with ZN_API_KEY set, the Node package sent tool output and tool-call arguments to api.usezn.com without redaction. 1.4.1 removes that path. Details in the CHANGELOG.

Performance and accuracy

We only publish numbers we can reproduce, with the method next to them.

  • Latency of the local engine: one table by input size, with hardware, date and the script to reproduce it (bench/bench_by_size.js), in the npm package README. Short summary from that run, with secret redaction (p50): about 0.045 ms for a 200 B prompt, 0.3 ms for a 4 KB tool output, about 4 ms for 64 KB.
  • Self-test: 51 curated vectors pass. That is a smoke test, not a detection or false-positive rate.
  • Known limitation: on whole source files, the rules can flag ordinary code comments. Version 1.5.0 reduces these on code and documentation, at the cost of letting some loosely worded override attempts through the local rules. To check it on your own source files, run zn-gate scan --code <path>; for the MCP servers you wrap, --shadow logs detections without blocking.
  • Known limitation: credentials under prefixed upper-case variable names (OPENAI_API_KEY=, MY_APP_DB_PASSWORD=, export X_SECRET=) are redacted in env files, JSON, YAML, Dockerfile and shell forms, and placeholders such as YOUR_KEY or ${VAR} are left alone (tested in the npm and PyPI packages). Lower-case or dotted names such as openai_api_key= or my.app.db.password= are not redacted by the prompt and tool-result rules.
  • Detection rates: on a private held-out set of 3,309 examples, Galvanize 2 has 83.4% recall at a 0.81% false positive rate (serving threshold 0.662); these numbers are for the cloud classifier only. They do not measure the local rules engine and are not protection that zn-gate gives you offline; the local engine has no detection rate, only the self-test and latency above. Method, per-language results, known weak spots and a reproducible benchmark are in the model card and the announcement. It is one layer of defense, not a guarantee.

What zn-gate is not

  • Not a sandbox. zn-gate checks what its hooks, MCP wrappers and library calls can see, and blocks or rewrites it. A program the agent starts can still do things the hooks cannot read. For that, use your agent's own sandbox or permission prompts (see the threat model).
  • Coverage depends on the agent. Each agent's hooks see and can change different things: for example, Cursor cannot rewrite shell or file output, and Codex replaces a result instead of editing it. Agent compatibility lists it per agent, and npx -y zn-gate check shows what your own agents are covered for.

Configuration

VariableDefaultEffect
ZN_CLOUDoff1 lets prompt analysis also query the cloud classifier. Requires ZN_API_KEY.
ZN_API_KEYunsetAPI key from usezn.com/dashboard. No effect without ZN_CLOUD=1.
ZN_LOCAL_ONLYunsettrue forces local-only, even with ZN_CLOUD=1.
ZN_STAGE / ZN_API_URLv30Cloud endpoint selection.

zn Team: zn team join <token> connects a machine to your organization's signed secrets policy and uploads its audit log (metadata only). What the organization can and cannot change on the machine, offline behavior and the OpenTelemetry export (zn vault log export --otlp): docs/team.md.

Custom rules: add banned patterns and forbidden paths in .znrules or zn.config.json at your project root (example).

Audit log: zn-gate shield appends each decision to ~/.zn/evidence.jsonl as a hash-chained (SHA-256) record with a redacted preview. It detects accidental or partial edits; it is not tamper-proof against someone with write access to the file. Check it with npx -y zn-gate evidence --verify. The agent hooks add to the same log when they block something or hide a secret in a tool result: it records the agent, the kind of event, a rule name and the kinds of secret, never a secret value, a command, a file path or your prompt.

GitHub Action

Guide for the audit mode (permissions, code scanning, badge): docs/github-action.md. To report findings without failing the build, see Warn without failing the build.

The action installs the published zn-gate packages at an exact version and runs on the runner only; it sends nothing anywhere. It has two checks, chosen with mode:

  • scan (default): zn-gate scan looks for prompt-injection traps and leaked secrets in prompts, tool definitions and config files (Python package from PyPI). This is what earlier releases of the action did.
  • audit: zn audit reviews what your coding agents are configured to trust: .claude/, .mcp.json, .codex/, .cursor/, hooks, AGENTS.md / CLAUDE.md, plugins and skills (npm package; needs version 1.6.0 or later).
  • both: runs the two. With a version older than 1.6.0, or a PyPI-only .post version, it runs the scan only and prints a warning.

Scan prompts on pull requests:

yaml
name: AI agent security scan

on:
  pull_request:
    paths:
      - 'prompts/**'
      - '**/*.prompt'
      - '**/AGENTS.md'
      - '**/SKILL.md'

permissions:
  contents: read

jobs:
  zn-gate-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Scan AI agent prompts and tools
        # Pin the action to a full commit SHA; the comment names the release.
        uses: usezn/zn@2afcaa17c7d1cb8fb5ded21f5f849ce8eba2bc5e # v1.14.1
        with:
          version: '1.14.1'
          package-sha256: '5ae5bae2625cad5fc1d0ca6d88a0d32f0e26310ef285d8adf48ddf68cb6983ba'
          path: '.'
          fail-on-threat: 'true'
          format: 'github'

Audit agent configs and plugins on every pull request, and show the findings in code scanning. The audit takes seconds, so the example has no paths: filter: a filter would skip the configs of agents it does not list.

yaml
name: AI agent config audit

on:
  pull_request:

permissions:
  contents: read

jobs:
  zn-gate-audit:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write   # only for the SARIF upload
      actions: read            # private repositories only, for upload-sarif
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Audit agent configs
        id: zn
        # Pin to the full commit SHA of an Action release v1.10.0 or later.
        # The zn-gate package version is set by version below.
        uses: usezn/zn@2afcaa17c7d1cb8fb5ded21f5f849ce8eba2bc5e # v1.14.1
        with:
          version: '1.14.1'
          mode: 'both'
          format: 'sarif'
          severity-threshold: 'medium'   # stricter than the default (high)
      - name: Upload SARIF
        if: ${{ !cancelled() && steps.zn.outputs.sarif-file != '' }}
        uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        with:
          sarif_file: ${{ steps.zn.outputs.sarif-file }}

The mode, agents, fail-on, severity-threshold, sarif-file and npm-integrity inputs and the audit-exit-code output need two things: an Action release of v1.10.0 or later (the ref in uses:) and a zn-gate package of 1.6.0 or later (the version input). The default version already meets the second. With uses: pinned to an older commit, the runner only warns about the unknown inputs ("Unexpected input") and the step runs a plain scan, which can pass: no audit runs.

InputDefaultDescription
version1.14.1Exact zn-gate version to install. Ranges and latest are refused. audit needs 1.6.0 or later.
modescanscan, audit or both.
path.File or directory to scan. audit needs a directory: with both a file skips the audit with a warning; with audit the step fails.
allow-partialfalseScan only: true keeps the step green when some files or folders were not read (2 MiB or larger, cannot be read, not a regular file, binary .key file); they are still listed. A scan that read no file at all still fails. Needs zn-gate 1.14.0 or later (an older version stops with an error).
fail-on-threattrueFail the step on findings. For scan, anything found (an injection trap or a secret); for audit, findings at or above severity-threshold. Set false to report only. true or false, in any case; other values fail the step.
severity-thresholdhighAudit only: lowest severity that fails the step when fail-on-threat is true: critical, high (high and critical), medium or low. The scan has no severities, so any scan finding fails.
fail-onemptyAudit only: lowest severity that fails the step (low, medium, high, critical, none), passed as is to zn audit --fail-on. When set, it overrides fail-on-threat and severity-threshold for the audit.
agentsemptyAudit only: comma-separated agents to audit (zn audit --agents). Empty audits all supported agents.
formatgithubgithub (inline annotations), text, json or sarif.
sarif-fileemptyWith format: sarif, where to write the file: inside the workspace or the runner temp directory, and not a symbolic link or under one. Empty writes zn-gate.sarif in a new directory under the runner temp directory.
package-sha256emptyRequires v1.5.0 or later of the action. SHA-256 of the PyPI wheel for version, as listed on PyPI. When set, the install fails if the downloaded file has a different hash. Older releases of the action ignore this input with only a warning, so no hash is checked.
npm-integrityemptyIntegrity string (sha512-...) of the npm tarball for version. When set, the audit fails if the tarball has a different hash.
OutputDescription
sarif-filePath of the SARIF file (format: sarif only). Empty if no check produced a report (for example, the scan ended in an error) or the file could not be written.
scan-exit-codeExit code of the scan: 0 clean (or fail-on-threat: 'false'), 1 something found (an injection trap or a secret), 2 error, 3 (zn-gate 1.14.0 and later) no file was read, or a file or folder was not read and allow-partial is not true. The step then fails with its own message; with format: sarif the report is still written. Empty if the scan did not run.
audit-exit-codeExit code of the audit: 0 clean, 1 findings at or above fail-on, 2 error. Empty if it did not run.

The scan finds more secret types with each release (1.9.1 added secrets passed as command-line flags and assigned to names such as password or api_key), so a job that fails on findings can start failing on files that passed before: pin version and read the findings before you update it.

Older releases of the action ignore the inputs they do not have, with only a warning: with one of them, mode: audit runs the scan instead.

Which audit findings fail the step: fail-on if it is set; otherwise none when fail-on-threat is false; otherwise severity-threshold. For the scan, only fail-on-threat applies.

With mode: both and format: sarif, the file holds one SARIF run per check (zn-gate scan and the audit's own run), each with its own category, so code scanning tracks their alerts separately. The step fails with the worse of the two results: an error beats findings.

Permissions: the action itself needs only contents: read; add security-events: write to the job only when you upload SARIF (and, in private repositories, actions: read, which upload-sarif needs). Packages are installed without running install scripts or build steps (--only-binary=:all: --no-deps for the wheel, --ignore-scripts for the npm tarball, whose dependencies are bundled) into private directories under the runner temp directory. pip still reads the runner's own pip settings (PIP_* environment variables and pip.conf), so a mirror or proxy you configure there is used; set package-sha256 so that the wheel is checked whatever index it comes from (and npm-integrity for the npm tarball). Run it on pull_request, never on pull_request_target: do not check out untrusted pull-request code in a pull_request_target workflow. With mode: audit or both the action also sets up Node.js 22 for the rest of the job.

pre-commit

Run both checks before each commit with the pre-commit framework (needs Node.js 18 or later; ids zn-gate-scan and zn-audit). The hooks file ships from release 1.14.0, so rev must be a release tag of 1.14.0 or later: earlier tags have no .pre-commit-hooks.yaml and pre-commit fails with InvalidManifestError. Add repos: [{repo: https://github.com/usezn/zn, rev: v1.14.1, hooks: [{id: zn-gate-scan}, {id: zn-audit}]}] to .pre-commit-config.yaml. Each tag's hooks run the exact zn-gate version of that tag (npx --yes zn-gate@1.14.1 ...), so the rev you pin decides which version runs on every commit. The scan hook reads the whole repository, so a file of 2 MiB or more makes it exit 3; to accept skipped files, add args: [--allow-partial] to the zn-gate-scan hook.

Repository layout

  • packages/zn-gate: Node.js CLI, MCP server and shield proxy (npm).
  • packages/zn-gate-py: Python SDK and CLI (PyPI).
  • src/: legacy Rust gateway, not released (docs/rust-core.md).
  • bench/: benchmark scripts and results.
  • assets/brand/: logo files (SVG master and light/dark variants, PNG lockup and banners).

Examples

Runnable examples are in examples/. The code API checks text for sensitive paths, prompt injection in tool output and secrets, and redacts the secrets; it does not block destructive shell commands.

ExampleLanguageFrameworkWhat it shows
python-basicPythonNoneEvaluate a prompt and guard a tool function.
node-basicJavaScriptNoneInspect a prompt and sanitize a tool result with the Node.js package.
custom-rulesJavaScriptNoneLoad project rules from .znrules.
langchain-agentPythonLangChain / LangGraphWrap tools so a blocked call does not run and secrets in tool output are hidden; a callback handler stops the run on a block.
crewai-agentPythonCrewAICheck what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
llamaindex-agentPythonLlamaIndexCheck what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
ag2-agentPythonAG2Check what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
openai-agents-sdkPythonOpenAI Agents SDKCheck what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
pydantic-ai-agentPythonPydantic AICheck what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
google-adk-agentPythonGoogle ADKCheck what a tool returns before the model reads it: a page that gives the model orders is replaced by a notice, and a secret in it is hidden.
langchain-jsJavaScriptLangChain.js / LangGraph.jsBlock a dangerous tool call and scrub what a tool returns before the model reads it.
openai-agents-jsJavaScriptOpenAI Agents SDK for JavaScriptBlock a dangerous tool call and scrub what a tool returns before the model reads it.
promptfooYAMLPromptfooRun a deterministic guardrail suite with Promptfoo.
github-actionYAMLGitHub ActionsScan agent prompts and configs in pull requests with the GitHub Action.

FAQ

Do I need an account or API key? No. Everything local works without one. An account is only needed for the optional cloud (team audit and the larger detection model).

Does zn-gate slow my agent down? The local check adds under a millisecond for typical prompts and tool outputs (p99 0.8 ms at 4 KB on our test host). See the measured table above.

Will it block legitimate content? Sometimes. Use init --shadow or shield --shadow to log detections without blocking while you evaluate it. Details in docs/shield.md.

Can zn-gate block keys in tool output? Yes. When a tool returns a secret value (an AWS, OpenAI or Anthropic key, a GitHub token, a JWT, a private key or a password in a database URL), zn-gate replaces each value it recognises with a marker or redacts it before the model sees it. "Block" here means that rewrite of the output; the tool call itself is not stopped. What is covered depends on the agent (the full table is in docs/compat.md):

  • Claude Code and OpenCode: every tool, including shell commands, file reads, search and MCP.
  • Codex CLI: the result is replaced by its scrubbed text, so the model gets text instead of the structured result. Codex's own session log keeps the original output.
  • Cursor: MCP tool output only. Cursor's hooks cannot rewrite shell or file-read output.
  • ZCode: zn hooks install zcode blocks reads and writes of secret files, shell commands that handle secrets, MCP calls that would send a secret and prompts that contain one. It cannot rewrite what a tool returns, so a value a command prints still reaches the model (zn adds a notice). ZCode ignores the hooks of a project. Details: docs/editors/zcode.md.
  • Copilot CLI (experimental): zn hooks install copilot blocks reads and writes of secret files and shell commands that handle secrets, and replaces secret values in the results of read-type and shell tools. It cannot block a typed prompt, lets a call through when its hook times out (zn sets a 15-second timeout on its hooks and blocks the call itself at 11 seconds), has not been tested on Windows, and Copilot's own session log keeps the original output. Details: docs/compat.md.
  • Gemini CLI (experimental): Google replaced Gemini CLI with Antigravity CLI for free and Google One accounts on 18 June 2026; Gemini CLI keeps working with zn-gate. zn hooks install gemini blocks reads and writes of secret files and shell commands that handle secrets. It does not change what a tool returns, so a value a command prints still reaches the model. Details: docs/compat.md.
  • Hermes Agent and the Cursor CLI: not covered yet. For any MCP client, the output of an MCP server wrapped with zn-gate shield or init is scrubbed.

For Codex CLI, zn also reviews the model's final answer. By default it only warns you; with "stopReview": "block" under hooks in ~/.zn/config.json, Codex asks the model to rewrite a final answer that contains a secret value. A second opt-in, "replaceToolOutputOnSecret": true under hooks in the same file, makes Codex replace a tool result that holds a real secret with zn's stop text and carry on, so the model gets that text instead of the output. See docs/compat.md.

Which agents are supported? Claude Code, Codex CLI, Cursor, OpenCode, Antigravity and ZCode desktop app, through hooks (what each one supports: docs/compat.md). Other editors (VS Code, Windsurf, Cline, Zed, JetBrains, Continue) work as an MCP server only: setup for each one. init can also wrap the MCP servers you already use.

How do I remove it? Run npx -y zn-gate uninstall in your own terminal. It lists what it found (the Claude Code plugin, zn's hooks for each agent, wrapped MCP servers), asks before removing, and ends with zn-gate check. Your vault is only deleted if you confirm that separately. npx -y zn-gate uninstall --dry-run shows the list and changes nothing. To remove one part at a time, or for the steps by agent, see the removal guide. init --revert is narrower: it only puts back the MCP configs that were saved before init --wrap-mcp wrapped them. For a standalone server, remove the zn-gate entry from your agent config.

Security

Report vulnerabilities to security@usezn.com. Policy, scope and safe harbor: SECURITY.md. Hall of Fame: usezn.com/security.

Releases published by the release workflow, starting with the first one after 1.5.0-beta.1, are signed and carry build provenance. 1.5.0-beta.1 and earlier versions were published by hand and have no signatures or provenance. To check a download: docs/verify-release.md.

Community and support

  • Discussions: questions about setup and use, ideas, and show and tell. Start here if you are not sure where your question belongs.
  • Issues: reproducible bugs and concrete feature requests.
  • Security vulnerabilities: do not post them in Discussions or Issues. Report them privately as described in SECURITY.md.

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md.

License

MIT. Copyright (c) 2026 usezn (usezn.com). The Galvanize 2 and Galvanize-60M weights are Apache-2.0. The zn-prompt-injection-bench dataset is CC-BY-4.0. The npm package bundles third-party code listed in THIRD_PARTY_NOTICES.md.