Repository navigation
Releases: usezn/zn
Release list
zn-gate 1.10.0
zn-gate 1.10.0 adds an audit mode with SARIF output to the GitHub Action, makes the commands that only you may run harder for an AI agent to start, gives the right repair in zn-gate check and zn-gate init when the zn-gate plugin is broken next to zn's own hooks, and checks long shell commands much faster. Update with npm install -g zn-gate@1.10.0 and pip install -U zn-gate.
If you run zn unlock from a script with the system keychain: it now needs an interactive terminal (--passphrase-stdin with the passphrase backend still works from a pipe). If you scan docker-compose files or .env.example: database URLs whose password is only a reference such as ${PGPASSWORD} are no longer reported.
Added
- GitHub Action:
modeinput (scan,audit,both; defaultscan).auditrunszn auditfrom the npm package on agent configs, hooks, MCP servers, plugins and skills;bothruns the scan and the audit, and runs the scan only, with a warning, whenversionis older than 1.6.0 or is a PyPI-only.postversion.mode: scankeeps the previous behaviour. - GitHub Action:
format: sarifwrites a SARIF file with one run per check and exposes it as thesarif-fileoutput, forgithub/codeql-action/upload-sarif. New inputsseverity-threshold(critical,high,mediumorlow; defaulthigh: the lowest audit severity that fails the step whenfail-on-threatistrue),fail-on(passed tozn audit --fail-on; overridesfail-on-threatandseverity-thresholdfor the audit),agents,sarif-file(inside the workspace or the runner temp directory, never through a symbolic link) andnpm-integrity; new outputsaudit-exit-codeandsarif-file.
Changed
- GitHub Action: the default
versionis now 1.9.0. - GitHub Action:
fail-on-threatmust betrueorfalse, in any case (TrueandFALSEstill work); any other value now fails the step instead of being read astrue. - Contributors: a pull request describes its change in a
changelog.d/fragment instead of editingCHANGELOG.md, and the release collects the fragments into its section. A newChangelogcheck asks for a fragment when a pull request changes what ships.
Fixed
- The agent hooks check a long shell command faster: each word is looked up once per command, so a command of 80 KB that repeats its words takes well under a second instead of several seconds. A hook that runs out of time lets the command go ahead unchecked, so this also keeps long commands checked.
POLICY_RULES_VERSIONis2026-10-16.6. zn-gate checkandzn-gate init: when both the zn-gate plugin and the hooks ofzn hooks install claude-codeare set up and the plugin does not work (for example on a Node.js the plugin does not support), they now check those hooks too, and run them the way Claude Code does. If the hooks work, the fix is to turn the plugin off withclaude plugin disable zn-gate@usezn, which keeps them, instead of removing the hooks. If the hooks do not work either, the hooks are repaired first (npm install -g zn-gate, thenzn hooks install claude-code) and only then is the plugin turned off, so the plugin is turned off only after the hooks are repaired. A Claude Code setting that stops all hooks (disableAllHooks, managed settings) is reported as such, and neither the plugin nor the hooks are touched.zn-gate checkandzn-gate init: when zn's own rules do not block the test.envfile, the fix is now to install zn-gate again (npm install -g zn-gate), also with the zn-gate plugin; before, they asked to install the plugin that was already installed.zn-gate init: with a broken plugin next to zn's own hooks that do not work either, it now checks that the repaired hooks work before it turns the plugin off, and leaves the plugin on when they still do not.- A database URL whose password is only a reference to a name (
postgres://${PGUSER}:${PGPASSWORD}@db/app,$PASS,%PASS%,<password>,{{ password }};${name}may also be lower case) is no longer reported or redacted as a secret, in Node.js and Python alike. Anything else is still a password, including a lower-case$wordor%word%and digits inside<>or{{ }}.RULES_VERSIONis2026-10-16.6.
Security
- Commands that only you may run (
zn secret setand the other secret commands,zn unlock, the guidedzn-gate init) now also check the parent processes for the AI agents' command-line programs (claude,codex,opencode,cursor-agent), which makes it harder for an agent that removed its environment variables to run them. It is not a guarantee: a process can detach from its parent, and the hook that denies these commands to agents stays the main guard. Editor apps such as VS Code or Cursor are not counted, so their integrated terminals keep working. zn unlockwith the system keychain now needs an interactive terminal, since that unlock asks nothing.--passphrase-stdinstill works from a pipe.
How to verify these files: docs/verify-release.md
zn-gate 1.9.1
zn-gate 1.9.1 finds more kinds of secrets with zn-gate scan and says what to do about each one, gives repairs that work in zn-gate check and zn-gate init (also after a Node.js version is removed), and closes ways an AI agent could start the guided zn-gate init. It also stops clearly on Node.js older than 18. Update with npm install -g zn-gate@1.9.1 and pip install -U zn-gate.
If a CI job runs zn-gate scan: the Node.js scan now reports more secret types, so a job that fails on findings can start failing on files that passed before. If you use the agent hooks: a prompt that holds an opaque Authorization: Bearer token is now blocked, and one in a tool's output is replaced.
Added
zn-gate scanfinds more secrets, in Node.js and Python alike: values of--api-key=,--password=,--token=,--secret=and similar flags on a command line; quoted values assigned to a name that ends in password, secret, token or api key with=,:,:=or=>(also$password = "..."), when the value looks machine-generated; an opaqueAuthorization: Bearertoken; and the password inhttps://user:password@hostandssh://user:password@hostURLs, such as git remotes. A flag without a value, a reference ($TOKEN,${API_KEY}), a placeholder (<token>,changeme) and a URL without a password are not reported. The Node.js scan now also reports the provider token formats the Python scan already reported (Slack, Stripe, Google, Hugging Face and the others the scrubber knows), with the same rule names.zn-gate scanreads UTF-16 files that start with a byte order mark, and the private key filesid_xmssandidentity.- The scrubber also replaces an opaque
Authorization: Bearertoken (new typeBEARER_TOKEN), as the Python package already did; zn's own__ZN_<NAME>__markers are left alone. GitHub's fixedx-oauth-basicpassword (https://<token>:x-oauth-basic@host) is no longer a URL password in either package; the token in the user part is still found by its own pattern. A password that is only a reference (%NAME%,$NAME,${NAME},<name>,{{name}}) is not one either, while a literal password is redacted whatever the user part says.RULES_VERSIONis now2026-10-16.4andSCRUB_RULES_VERSION2026-10-16.3.
Changed
- With Node.js older than 18, the
zn-gateandzncommands you run stop with a short message that names the version and how to install Node.js 18 or later (exit code 1). Hooks, MCP servers and thezn runcommands that zn's hooks write keep running and print a warning, so a check that works today is not turned off. Node.js 16 and 17 were never supported; some commands happened to run on them. - The Python
zn-gatesays that commands such ashooks,init,checkandunlockare in the npm package and givesnpx -y zn-gate init, instead of argparse's "invalid choice" error. zn-gate check: when a hook cannot start because its Node.js or its zn-gate files are gone (for example afternvm uninstall), the fix it gives is a freshnpm install -g zn-gateand thenzn hooks install <agent>, since the old Node.js took itsznwith it. Claude Code set up withzn hooks installis told to run that again rather than to install the plugin. The zn-gate plugin together with the hooks ofzn hooks install claude-codeis reported as not protected, because both run and a broken one can block every prompt; the fix iszn hooks uninstall claude-code. A zn-gate plugin that is turned off is fixed withclaude plugin enable zn-gate@usezn; next to zn's own hooks, which then are the ones that protect Claude Code, it is only noted.zn-gate initfollows the same routes. A Claude Code prompt hook withoutonFailure: "block"gets a note.- The Python source distribution is now reproducible: two builds of the same commit with the same build tools give the same file, byte for byte, as the wheel and the npm tarball already did. CI and the release workflow build it twice and fail if the two differ.
zn-gate scansays "Secret found" for a secret, instead of calling it a threat, and adds one next step in plain words: runnpx -y zn-gate env shadow <file>to move the values of a.envfile into the local vault (the file name is shown only when it is made of plain characters and does not start with-, otherwise the command says<file>), runnpx -y zn-gate secret set NAMEto store any other value, move a private key out of the file, and rotate anything that was ever committed or shared. The value is never printed. In--format github, a secret's annotation is titledSecret found (<rule>)and its message is the next step; the summary line readsFound N secrets and M security threats.In--format json, each secret has a newnext_stepfield and the report has a newtotal_secretscount;total_threatsstill counts every finding and the other fields are unchanged. Injection findings keep their wording.- The Node.js scan reports more secret types than before (the provider formats, Bearer tokens, URL passwords, command-line flags and assignments above), so a CI job that fails on findings (
fail-on-threat, or the exit code ofzn-gate scan) can start failing on files that passed before. - Agent hooks (Node.js): a prompt that holds an opaque
Authorization: Bearertoken is now blocked, and one in the output of a tool or MCP call is replaced, as for the other secret formats the scrubber knows. Usezn secret set NAMEand the__ZN_NAME__marker in its place. - Docs: the Python install steps say that Debian and Ubuntu need the
python3-venvpackage forpython3 -m venv. - Docs: what to do when your agent blocks every prompt because zn's hook cannot run (usually after Node.js is upgraded or moved):
zn-gate check, reinstall, or turn zn off for that agent until it is fixed (docs/compat.md). - Docs: the README, docs/secrets.md and docs/check.md describe the 1.9.1 behaviour: "Secret found" and its next step in
zn-gate scan, the secret types it finds (and that a CI job can start failing on files that passed before), the Node.js 18 message, the repair steps ofzn-gate check, the guidedzn-gate initwithout arguments, andzn hooks uninstall claude-codekeeping othersecretsMCP servers.
Fixed
zn hooks uninstall claude-coderemoved any user MCP server calledsecretsfrom Claude Code, also one that was not zn's. It now removes only zn's own server, recognised by its exact command (zn-gate'ssecrets-mcp), and says so when it leaves another one in place.zn hooks installno longer overwrites anothersecretsserver that only mentionssecrets-mcp.- An AI agent could start the guided
zn-gate initdespite the rule that keeps agents from running it: with a backslash or other characters the shell joins into one word, an option in capital letters, ornpm exec, which keeps the options after the package for itself. These are refused now, and the guidedzn-gate initstops with an error when it gets any argument. An agent variable such asCLAUDECODEthat is set but empty now also counts as an agent.POLICY_RULES_VERSIONis2026-10-16.5.
How to verify these files: docs/verify-release.md
zn-gate 1.9.0
zn-gate 1.9.0 makes npx -y zn-gate init the one command that sets up zn for the coding agents on your machine, and adds zn-gate check to show that zn stops your agent from reading a secret. Block messages for commands only you may run now give the exact command. Update with npm install -g zn-gate@1.9.0; the Python package has no code changes and moves to 1.9.0 to keep the versions in step.
Breaking for scripts: zn-gate init without options no longer wraps the MCP servers in your agent configs. Use zn-gate init --wrap-mcp.
Added
zn-gate initis now a guided setup: it finds Claude Code, Codex CLI, Cursor and OpenCode, shows the one route that protects each agent that is not protected yet (the zn-gate plugin for Claude Code,zn hooks installfor the others, from a global install rather than the npx cache), runs each step after you confirm it in your own terminal, and ends withzn-gate check. When an AI agent or a script runs it, it only prints the steps.zn-gate check(alsozn-gate doctor) shows that zn stops your coding agent from reading a secret. For each agent set up for your user (Claude Code through the zn-gate plugin orzn hooks install, Codex CLI, Cursor, OpenCode), it asks zn's hook to let the agent read a.envfile with a fake key and an ordinary file, in a temporary folder. An agent counts as protected only when the first is blocked and the second allowed. For Claude Code, Codex CLI and Cursor it also runs the installed hook, which finds a hook that cannot start or that blocks every file (on Windows, hooks installed aszn hook <agent>are checked against zn's rules instead, and the output says so). It says how to fix an agent that is not protected. Nothing is sent over the network and no setting changes. See docs/check.md.zn hooks install claude-codeandzn-gate statuswarn when Claude Code settings stop zn from running: an organization policy that turns off all hooks (disableAllHooks), runs only its own hooks (allowManagedHooksOnly), or does not allow thesecretsMCP server (managed-mcp.json,allowManagedMcpServersOnly,allowedMcpServers,deniedMcpServers), ordisableAllHooksin your own settings. Claude Code does not show these. Only the settings files are read, not policy sent through claude.ai, MDM or the Windows registry.
Changed
- The READMEs start with
npx -y zn-gate init; the steps for each agent are under "Manual install". - Block messages, third batch: zn commands that only the user may run (
hooks installanduninstall,unlock,lock,stats share on,secret,env shadow,pins approve,init) each say what the command does and the command for the user to run, with the agent, secret name or file name when the agent's command had one, instead of one message about secret values for all of them. Reading or changing~/.znnow says how a command can still use a secret, and a secret after a shell keyword says why it cannot be filled in. - Breaking for scripts:
zn-gate initwithout options no longer wraps the MCP servers in your agent configs: that is nowzn-gate init --wrap-mcp.--dry-run,--shadowand--revertstill select it. The guidedzn-gate initruns only in your own terminal and asks before each step; zn's hooks also refuse the usual ways an AI agent would start it, as they refusezn hooks install(the zn-gate plugin for Claude Code gets this rule once it is pinned to 1.9.0).--wrap-mcpis not affected. - CI: draft pull requests skip the test, package, plugin, Rust, Windows and action jobs until they are marked ready for review, and pushes that change only documentation skip the test and plugin workflows (Markdown in the packages and plugins still runs them); the plugin checks also skip documentation-only pull requests. On release pull requests, which are opened before the plugin can be pinned, a plugin pin one step behind the release is a warning in
Public facts; it still fails onmainandnext, somainstays red after a release merge until the plugin is pinned. ThePlugin repositoryworkflow ends with a warning instead of failing while the plugin repository's copy of its checks is out of date. - CI: pushes to
nextleave out the macOS jobs of the leak bench and the scrubber and vault tests, and run the plugin checks on macOS with Node.js 22 only; Ubuntu and Windows still run every version. The full matrix still runs onmain, on pull requests intomainand release branches, with thefull-cilabel and on manual runs. A new push tonextcancels the CI runs still going for the previous one; runs onmainalways complete. - CI: the Rust build cache is saved only from
main;nextand pull requests restore it and save nothing, so the repository stays under its 10 GB Actions cache limit. A new weeklyCache cleanupworkflow deletes the caches of closed pull requests and can be run by hand as a dry run. - The hash-pinned tool lists used by the CI and release workflows (
.github/ci-test-requirements.txt,.github/release-requirements.txt) now say how to regenerate them with uv when a security alert names them, since Dependabot cannot update them.
How to verify these files: docs/verify-release.md
zn-gate 1.8.2
zn-gate 1.8.2 adds MCP tool annotations. Nothing else changes; the Python package moves to 1.8.2 to keep the versions in step. Update with npm install -g zn-gate@1.8.2.
Added
- MCP tool annotations: every tool of the zn-gate and secrets MCP servers now has a title and says whether it is read-only, destructive, idempotent and reaches the outside world (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), so MCP clients can show it and ask before the tools that change things. analyze_prompt reaches the outside world only with the optional cloud check on. No change in what the tools do.
How to verify these files: docs/verify-release.md
zn-gate 1.8.1
If you use the zn-gate agent hooks with Claude Code, Codex CLI, Cursor or OpenCode, update. This release reads Codex and OpenCode patches the way those agents read them, and makes the secret scrubber and several other checks run in linear time. Update with npm install -g zn-gate@1.8.1; the Python package has no code changes and moves to 1.8.1 to keep the versions in step.
Fixed
- The vault audit log no longer slows down when the end of the file holds a long run of blank lines before a large record. Reading the last record used a regular expression that took seconds in that case, on every new audit record.
- File names in a Codex or OpenCode patch (
apply_patch), values in.env,.npmrcand.znrulesfiles, and a fewzn auditchecks are now read in linear time. Long runs of blank lines, spaces,*or=used to make them take seconds.SCRUB_RULES_VERSIONis now2026-10-15.2andAUDIT_RULES_VERSION2026-10-15.1. - File names in a Codex or OpenCode patch (
apply_patch) are read the way each agent reads them: lines split only at line feeds, and names in every form Codex or OpenCode could use. A patch with a name zn cannot read safely, or with a line that looks like a file marker but is not one, is blocked.
Changed
- In Codex and OpenCode, a patch (
apply_patch) longer than 100 KB is blocked, like shell commands over 100 KB, because zn could not check it within the time the agent waits for the hook.
How to verify these files: docs/verify-release.md
zn-gate 1.8.0
zn-gate 1.8.0 adds Warn rules for zn Team: an organization can have a secret use go ahead with a notice and an audit record instead of blocking it. Organization policy now also applies to secrets_render_file and to marker restores in the agent hooks. zn-gate shield pins the tools of the MCP servers it wraps, and block messages are written in plain words.
After updating, run zn unlock once. A vault that is still running from an older version keeps secrets as markers and tokens in the agent hooks until it is restarted; zn unlock restarts it.
Going back to 1.7.1 in a zn Team organization with Warn rules: an organization policy with warn rules stored by 1.8.0 is refused by 1.7.1, which then applies only your local settings until the next policy sync downloads a copy it can read. Stay on 1.8.0 or later in such an organization.
Added
- Codex plugin layout:
plugins/tools/check-plugins.jsnow checks the root manifest'sextensions.com.openai.hookspath and the repository marketplace entry against OpenAI's Codex plugin documentation. - MCP tool pinning:
zn-gate shieldrecords a hash of each tool of a wrapped server (the whole tool definition) from the first tool list it sees for that server (all its pages), and compares every later list with it. By default a tool that is new or changed since gets a notice at the start of its description that tells the model to ask you first, and the shield writes a warning; tools with a duplicated or invalid name are refused. With"mcpPinning": "block"in~/.zn/config.json(orZN_MCP_PINNING=block) such tools are removed and calls to any tool that is not approved are refused. Only your own~/.zn/config.jsoncan make the mode weaker thanwarn; the environment of an MCP server can only make it stricter.zn-gate pinslists the servers and what changed;zn-gate pins approve <server>accepts the changes, only in an interactive terminal, and the agent hooks refuse it from an agent. Pins are kept in~/.zn/mcp-pins/(the program, its arguments with secrets redacted, tool names and hashes). Limits: pinning covers tool definitions, not the server's code; the first list of a server is trusted, and so is the first list after its command line changes (for example a new package version); approving accepts the tools as they are listed then (the pins hold names and hashes, not the tool text); an agent that can run code on your machine can edit the pins or start a server without the shield, and the hooks only stop the commands they recognise (POLICY_RULES_VERSIONis now2026-10-15.13). - CI: a
Public factscheck fails a pull request when the release version, the agents with hooks, the license or the install command differ between the package and plugin manifests, the MCP Registry files and the pinned versions in the documentation (.github/public-facts.json). - zn Team: a policy rule can warn instead of block (
"warn": true). The use goes ahead, an audit record with the decisionwarnis written and uploaded to your organization, and you and the agent seezn: your team policy would block <NAME> here (policy note: "<reason>"). It was allowed and recorded for your organization.It applies wherever a block applies (zn run,secrets_http_request,secrets_render_file); a rule with bothdenyandwarnmakes the policy invalid. zn-gate now sendsX-ZN-Policy-Features: warnwhen it downloads the policy, so the server sendswarnrules only to versions that understand them. The OpenTelemetry export mapswarnto severityWARN.
Changed
- Block messages in plain words. When zn cannot read a credential file for the agent, the message names the file, says why and how to use the value instead (
zn secret set NAME, then__ZN_NAME__in the command). A secret used in the wrong place of a shell command gets one short explanation and an example. When a command is not allowed to use a secret,zn runsays so and gives the command to allow it. When the hook itself fails, the agent reads that the action was blocked and, if it happens again, to ask you to runzn hooks install, followed by the error detail. The text that replaces a tool output with instructions aimed at the agent now tells the agent not to act on it and to tell you. - Block messages in plain words, second batch. PowerShell refusals, MCP and Monitor notices, and the notes on commands with secret placeholders now say what happened and give one next step. Errors a person reads gain a specific next step where they are raised, written for a person first and for an agent second; the "Not allowed" and "Not found" prefixes are unchanged. The Python guard returns one message with the reason and a next step: a blocked input says not to retry with reworded text, and a blocked tool result says not to use that result. The Python block text no longer shows the confidence value; read the rule and reason from the exception's
assessmentattribute. The default CrewAI fallback text changed: passfallback=to keep a fixed string. (POLICY_RULES_VERSIONis now2026-10-15.14.) - Python 3.8 and 3.9 are no longer supported (both are past their end of life); pip on those versions keeps installing zn-gate 1.7.1.
- When an MCP call is too large for zn to check (arguments over 1 MB), the message now tells the agent to tell you and not to resend the content in parts.
Fixed
- zn Team:
secrets_render_filenow applies an organizationdeny. Before, afilemode secret blocked by the organization policy could still be written to a file through this tool. - Plugin repository checks: the Verify workflow no longer stops without a message on a release pull request (the comparison of the two trees ended the step under
pipefail).
Security
- A value the scrubber replaced with a token is not put back when it is also a stored secret that the organization's policy blocks. When the vault process was started by an older zn-gate, the hooks keep tokens as they are instead of restoring them and say so to you and the agent;
zn unlocknow restarts such a vault, andzn vault statusshows the running vault's zn-gate version and flags an outdated one. - Marker restores in the agent hooks now apply the organization's policy. When an agent writes a
__ZN_NAME__marker into a git-ignored file, a secret the zn Team policy blocks stays a marker and the hook says why; a secret under awarnrule is restored, recorded with the decisionwarn, and the hook's message shows the warning. - When an event cannot be read or part of zn-gate is missing, the message the agent reads no longer says how to let actions pass unchecked (
hooks.whenUnavailable). That setting is still documented and is shown only to you (as the hook's message to the user in Claude Code). - When an agent event is not valid JSON, the refusal no longer quotes the part of it that Node's parser shows, which could be tool output.
- The agent hooks again refuse zn admin commands written with a Windows path or
.cmd(for exampleC:\Users\me\AppData\Roaming\npm\zn.cmd secret reveal NAMEor.\zn.cmd hooks uninstall). - Shell, PowerShell and Monitor commands over 100 KB are blocked instead of checked. Checking a command that long could take longer than the agent waits for the hook, and a hook that times out lets the command run unchecked. The message tells the agent to tell you and not to resend the command in parts (
POLICY_RULES_VERSIONis now2026-10-15.15). - zn Team: a
warnrule now also applies when the agent hooks put back a value the scrubber replaced with a token and that value is a stored secret under the rule: you and the agent see the notice, and the audit record has the decisionwarn.
How to verify these files: docs/verify-release.md
zn-gate 1.7.1
If you use the zn-gate agent hooks, update. This release makes the hooks' glob matching and path resolution run in linear time, and zn-gate scan for Node.js never prints matched values.
Security
zn-gate scanfor Node.js no longer prints matched values in any output format (the Python package already redacted them): the line shown with a finding also goes through the scrubber's secret patterns.scannow accepts--format jsonand--format githublike the Python CLI (the GitHub annotations show rule, reason, file and line only). Before, Node.js ignored--format; an unknown format now exits with code 2.- Glob matching, brace expansion and path resolution in the agent hooks run in linear time, and a bracket expression in a glob never matches a path separator, as in the shell (
POLICY_RULES_VERSIONis now2026-10-15.11).
How to verify these files: docs/verify-release.md
zn-gate 1.7.0
zn-gate 1.7.0 adds zn-gate stats (local counts of what zn-gate did), MCP argument checks in Cursor, a preview of the organization before zn team join, and an optional cloud classifier key for the Claude Code plugin. The Python package redacts the same provider tokens as Node.js, zn-gate scan lists the files it does not read, the Rust engine's rules are more precise, and the agent plugin launcher only loads verified files.
What can leave your machine, all off by default: with zn-gate stats share on, one pseudonymous report of counts a day (a random installation id, never values, prompts, names or paths); with the Claude Code plugin's API key, the text analyze_prompt checks, with secrets redacted, to api.usezn.com; and zn team join sends the join token to api.usezn.com for the preview before the confirmation dialog.
Added
- Claude Code plugin: an optional API key setting (
api_key, stored as a sensitive value) turns on the zn cloud classifier for theanalyze_prompttool only; the text it checks is sent to api.usezn.com with secrets redacted first. Without a key the plugin stays local, andZN_CLOUD,ZN_API_KEY,ZN_API_URLandZN_STAGEfrom the environment are still ignored. - Claude Code plugin: a listing icon for the Claude plugin directory (
icon.pngat the plugin root, named in the manifest), and the plugin's contact email is now support@usezn.com (security reports still go to security@usezn.com). zn-gate stats(Node and Python) shows what zn-gate did on this machine: checks, blocks, secret values replaced before the model saw them, and the secret types found, per agent, for the last 30 days (--days N,--json). The agent hooks, the MCP tools and the shield proxy keep these counts in~/.zn/stats.json(numbers only; never values, prompts, paths or hosts).ZN_STATS=offturns counting off. Nothing is sent: sharing pseudonymous daily counts is a separate opt-in (zn-gate stats share on,off,status,show), off by default; when on,zn-gate statssends one report per complete day with only the fields thatzn-gate stats share showprints first.- Sharing can only be turned on by you, in your own terminal, after a yes/no question: the agent hooks treat
zn-gate stats share onas an admin command an agent cannot run. Only built-in secret types are counted: a variable or secret name you chose is counted asOTHER(orENV_VALUEfor a value matched by its variable name), never by its name. zn vault teamsays that a shared vault for teams is planned and counts the request on this machine only. Nothing is sent and no contact details are asked for.- Cursor checks the arguments of MCP tool calls: a call that would send a secret value, or a
__ZN_marker, to an MCP server other than zn'ssecretsserver is denied. Runzn hooks install cursoragain to add this hook. zn team joinshows the organization name and who invited you in the vault's confirmation
dialog, fromPOST /v1/org/join-preview, before this machine sends its audit key. The preview
does not use up the join token; an invalid, expired or used-up token, or an organization without
an active subscription, stops before the dialog. The check is made only againstapi.usezn.com:
another server given with--apigets the token only after you confirm, as before. If the
organization joined is not the one shown, the join is refused and nothing is saved. Names from
the server are shown without control, zero-width or bidirectional characters.
Changed
zn-gate scan(Node.js and Python) reports the files it does not read: files of 2 MiB or more, binary.keyfiles and files that cannot be read (Python also lists pipes and other files that are not regular files, which it no longer opens). Node prints them after the summary; Python lists them in text output, as::noticelines with--format github, and asskippedwithtotal_skippedin JSON. Python's JSONtotal_filesnow counts only the files that were read.- Claude Code plugin: the manifest's homepage and documentation links point to usezn.com instead of a folder of this repository.
- The Rust engine's prompt-injection rules return a stable rule ID (for example
pi:ignore_previous; the block message reads "Prompt Injection blocked: pi:") and use a grammar based on the Node.js rules for "ignore / disregard / forget ... instructions", so phrasings such as "ignore the instructions above" or "ignore these instructions" are caught while "you can ignore the build instructions" is not. Phrases that are common in ordinary text are no longer blocked: "act as a", "respond as a", "debug mode", "root access", "shell access", "user message:", "end of conversation", "end of transmission", "stay in character", "prompt injection", "stop what you are doing", "without any restrictions", "imagine you are a", "into developer mode", "switch to developer mode" and "override system" (now only "override the system prompt" and similar). "system message:" and "new instructions:" count only at the start of a line, "forget everything" only at the end of a clause or followed by "above", "before" or "you know/were told", "do anything now" only as "you can do anything now" or with "DAN", and "jailbreak" must be a whole word. - The hook policy rules are at
POLICY_RULES_VERSION2026-10-14.10(see the entries below that change them).
Fixed
- The Rust engine's restricted-commands rule now matches whole words in tool names too, so tools such as
transform_data,perform_searchoradd_itemare no longer blocked asrmordd;format_disk,runShutdownandrmstill are. - The Rust engine's log scrubber now redacts card numbers only when they pass the Luhn check and match a card network, US SSNs only with a valid area, group and serial, and phone numbers only in E.164 form or as a formatted North American number, never a digit run inside a longer number. IDs and timestamps are no longer redacted as phone numbers.
- The agent hooks read glob bracket expressions as the shell does. A command with one that is not a valid regular expression (for example
[z-a]or[a\]) no longer makes the hook fail, and a]as the first member or a POSIX class such as[[:alpha:]]is now understood, so words like.e[!]]vor.en[[:alpha:]]are recognised as naming.env. zn auditno longer reports zn's ownsecretsMCP server as an unknown server when its command ends with the--agent <id>thatzn hooks installadds.zn initfinds the Antigravity MCP settings: it checks~/.gemini/antigravity/mcp_config.jsonand~/.gemini/config/mcp_config.json(each one only if it exists). It looked for~/.gemini/antigravity/mcp.json, which Antigravity does not use, so Antigravity was never found.- The threat model said that
zn runkeeps interpreter start-up variables such asNODE_OPTIONS; since 1.5.1 it removes them whenever it injects secret values.
Security
- The agent hooks' check for zn admin commands no longer takes exponential time on a crafted
zn statscommand with many flags, which could stall the hook. zn-gate scan --format github(Python) escapes file names and messages in its::errorand::noticelines, so a file name cannot add a line or a workflow command of its own to the CI log.- Agent plugin launcher: JavaScript modules that Node loads by itself (zn-gate's encryption libraries) can only come from the verified zn-gate files, and are run from the bytes that were checked. This covers ES modules, CommonJS and JSON that Node loads, and the require() calls inside them. It needs Node.js 22.15 or 23.5 or later for the hooks; the MCP servers have it on every supported version (before Node.js 22.15 without createRequire(), which the release build refuses). Before, a module missing from the package could be looked up in node_modules folders outside it, and the source was read from disk again after the check.
- The Python package (
zn-gateon PyPI) now redacts the same provider token formats as the Node.js scrubber: Stripe, Slack, GitLab, npm, PyPI, SendGrid, Google, Hugging Face, Twilio, AWS STS, Azure storage and AD secrets, DigitalOcean, Shopify, Mailgun, Groq, Replicate, xAI, Docker Hub, Tavily, Firecrawl, Polar, Supabase and other provider tokens, passwords in URLs, andAuthorization: Bearertokens. Generic assignments such aspassword = "..."in source files are not yet redacted by the Python package. Token-like runs of 24 or more characters that remain after redaction are masked in printed output and in the evidence ledger preview. zn-gate scan --format githubprints the rule, the reason and the file and line only. It no longer prints the matched line, so CI logs do not show credential text.- The evidence ledger directory is created with mode 0700 and the ledger file with mode 0600 on POSIX systems, in both the Node.js and the Python packages. A ledger that others can read is restricted on the next write.
How to verify these files: docs/verify-release.md
zn-gate 1.6.0
If you use zn-gate 1.5.x, update. In 1.5.x, zn-gate scan (Node.js) printed PASSED for a folder whose .env or other files held API keys, because it only reported prompt-injection text. 1.6.0 reports exposed credentials too (see 1.6.0-beta.1 below). After updating, run zn-gate scan again on your projects. This release also includes everything in 1.6.0-beta.1.
Added
- Every rule layer has a version:
SCRUB_RULES_VERSION(secret and PII patterns),AUDIT_RULES_VERSION(zn auditrules) andPOLICY_RULES_VERSION(hook policy).SCRUB_RULES_VERSIONis2026-10-14.2,POLICY_RULES_VERSIONis2026-10-14.4, andAUDIT_RULES_VERSIONis2026-10-14.1.zn-gate --versionandzn-gate statusshow the scrub and audit versions next to the rules engine version.
Changed
- Block and error messages now say what happened and what to do next, in plain words. The CLI prints the code (such as
ZN_LOCKED) only withZN_VERBOSE=1orDEBUGset, and the suggested commands (zn unlock,zn secret set NAME,zn env shadow FILE) fall back tonpx -y zn-gate@<version>whenznis not on your PATH. - Default policy by tiers. zn-gate still blocks what is almost certainly an attack or a leak: credential files, requests for secrets, exfiltration links, Base64 or invisible-character evasion, "ignore previous instructions", the narrow forms of the rules above and dangerous commands. Ten heuristic rules that also match ordinary text (
pi:identity_override,pi:system_tag,pi:system_bracket,pi:new_instructions,pi:pretend,pi:roleplay,pi:jailbreak,pi:bypass,pi:override,pi:template_boundary) now give an advisory by default instead of a block. Strict mode blocks them as before:ZN_STRICT=1,"strict": trueinzn.config.json, astrictline in.znrules, thestrictoption in Node.js orevaluate(text, strict=True)in Python. In our tests, benign texts blocked on a public prompt-injection dataset went from 8 of 7,150 to 0; injections blocked went from 24.0% to 22.4% and from 27.7% to 21.3% on two public datasets, and every injection that is no longer blocked gets an advisory.zn-gate testcounts an advisory on an attack sample as detected and shows it as ADVISORY.zn auditandzn-gate scankeep reporting these patterns (they use strict mode). The Pythoncheck_tool_call,check_tool_resultandsanitize_tool_resultreturn the advisory and takestrict=True. Strict mode set in the environment or a config file cannot be turned off by passingstrict: false.RULES_VERSIONis now2026-10-14.3. - Fewer false blocks from the local rules. Four rules blocked a lot of ordinary text, such as documentation, code and requests like "act as a gateway". They now block only the clear form, and the broader match is reported as an advisory: the verdict stays
allowand the result carriesadvisorywith the rule and a short reason (zn-gate analyze,analyze_prompt, Pythonevaluate()andanalyze()).exfil:credentialsblocks when a verb such as "send", "print" or "read" starts an instruction and a credential word ("API key", "password", "token",.env) follows in the same sentence, within 80 characters. Before, the two words could be anywhere in the text, so whole documents were blocked.indirect:hidden_tagblocks when the instruction-like word is inside the hidden element. Before, any later word such as "system" counted.pi:act_asblocks a request to the model to act as an assistant, a model, a persona or an unrestricted mode ("You will act as …"), not "act as a gateway".pi:forgetno longer blocks "forget all cached entries", "forget your password" and similar phrases about data.
In our tests on 747 public files (package READMEs and code, docs), files blocked by the local rules went from 5.6% to 1.1%; on two public prompt-injection datasets, benign texts blocked went from 2.1% to 0.1%, injections blocked went from 28.1% to 24.0% and from 30.9% to 27.7%, and every injection that is no longer blocked gets an advisory. The local rules are a first layer; the cloud check (ZN_CLOUD=1) is unchanged.RULES_VERSIONis now2026-10-14.2.
- CI fails when the local rules block more benign text. A new gate runs the rules engine over the pinned benign corpus and over a list of ordinary developer sentences, and compares the block counts per rule with a committed baseline.
- CI also fails when a rule layer changes without a higher version, or when its rule ids change without an updated snapshot. When the block counts fall, the gate passes and warns that its baseline is stale.
Fixed
zn unlockno longer hangs at "Repeat passphrase" when the second line arrives before its prompt (for example when both lines are pasted at once). Each prompt used its own line reader and the second line was lost, so the vault was never created andzn env shadowthen reported that the vault was locked. Wait for each prompt before typing: text typed before a prompt appears can be shown on screen by the terminal. Different passphrases, Ctrl+C and Ctrl+D now end with a clear message instead of waiting.
Security
- Agent hooks: an AI agent could run the zn commands that manage your secrets and protection (
zn unlock,zn lock,zn secret ...,zn env shadow|unshadow,zn hooks install|uninstall) by calling a specific package version, for examplenpx -y zn-gate@1.5.1 hooks uninstallornpm exec zn-gate@1.5.1 -- unlock: the hook only recognised the plainznandzn-gatenames. Most of these commands also refuse to run without an interactive terminal or when they detect a coding agent, which stopped the usual agents;zn lockhad no such check. The hooks now deny the versioned forms withnpx,npm exec,pnpm dlx,yarn dlxandbunx. Affected: 1.5.0-beta.1 to 1.6.0-beta.1. - Trigger words are now found in the same way in Node.js and Python. A non-ASCII letter next to a trigger word (for example
éjailbreak) no longer hides it: Python previously allowed this while Node.js blocked it, and both now block. - Compatibility characters are folded before matching: fullwidth Latin text such as
ignoreis treated asignore, in both languages. The folding is used only for detection; the text returned to you is not changed. - Invisible characters (zero-width spaces and joiners, the word joiner, variation selectors and Unicode tag characters) are ignored when matching, so a trigger word or a secret split by them is still detected.
- The dotless i (
ı) and the capital dotted I (İ) are read asiandIwhen matching, so trigger words spelled with them are detected in both languages. - A secret that contains invisible characters is redacted as a whole, including those characters. Before, a key such as
AKIAfollowed by a zero-width space was not redacted. - The Node.js scrubber finds such secrets too and reports their full span.
- The PII & secret scrubber now recognizes more credential formats: Docker Hub personal and organization access tokens (
dckr_pat_,dckr_oat_), Tavily keys (tvly-), Firecrawl keys (fc-), Polar tokens (polar_oat_,polar_pat_,polar_at_u_,polar_at_o_,polar_rt_u_,polar_rt_o_,polar_ci_,polar_cs_), Replicate tokens (r8_, 40 characters), Supabase personal access tokens (sbp_) and Azure AD client secrets. - Coverage fixes: Hugging Face tokens made only of letters, Stripe
sk_prod_keys, Twilio API key SIDs in upper case, and legacy 32-character hexadecimalsk-keys are now detected by the scrubber as well.
How to verify these files: docs/verify-release.md
Security advisory
This release fixes GHSA-wvv2-f3m6-6wrq (moderate): an agent could run zn admin commands through a versioned package call such as npx -y zn-gate@<version> hooks uninstall, skipping the hook that denies them. Update to 1.6.0.
zn-gate 1.6.0-beta.1
Pre-release for testing. Install it with npm install -g zn-gate@beta or pip install --pre zn-gate. The agent plugin in plugins/zn-gate is not published with this beta; it ships with 1.6.0.
Added
- Cloud checks of long texts (
analyze_prompt,zn-gate analyze, Pythonanalyze(), only withZN_CLOUD=1and a key): a text over 1,000 characters asks the classifier for overlapping 512-token windows (stride 256, up to 20) instead of reading only the first 512 tokens; the verdict comes from the window with the highest score, and a text that fits in one window gets the same result as before. In our internal tests (long documents with one hidden injection; results depend on the text), detection went from about 1% to 91% when it was in the middle and to 86% at the end; false positives on long benign documents went from 3.7% to 5.7%, and long texts take about three times as long (the client allows 12 s instead of 4 s). Cloud checks are billed by text size, with or without windows (offdoes not lower the count): 1 call up to 2,000 characters, and 1 more for each further 1,000 characters or part of them (up to 19 calls for the 20,000-character limit); the gate reports the count inbilled_calls. When more windows would be needed, or the gate falls back to checking only the first 512 tokens, the result carries awarning. Turn it off withZN_LONG_INPUT=off,--long-input offorlong_input="off". Needs a cloud gate that supportslong_input; an older one ignores the field. zn audit [path](alsozn-gate audit) reviews the agent configuration a repository ships before you open it, without running anything: hooks and other commands that run automatically, project MCP servers, unpinned packages, model endpoint and proxy overrides, code-loading environment variables, credential helpers, broad permissions, secrets, dangerous instructions inAGENTS.md,CLAUDE.mdand similar files, agent extensions, executables in the root that shadow common tools, and CI workflows that run an agent on untrusted input. It covers Claude Code, Codex, Cursor, OpenCode, Amazon Q, Copilot and Gemini CLI. Output as text, JSON or SARIF 2.1.0 (GitHub code scanning);--fail-onsets the exit code. RulesZNA001-ZNA020and the public vulnerabilities behind them: docs/audit.md. The secret-use log thatzn auditshowed in 1.5 iszn vault log(aliaszn secret log);zn audit logandzn audit --sinceexit with code 2 and point there.- zn Team client (
zn team join | status | sync | leave): connects this machine to a zn
organization (docs/team.md). The organization's policy is signed with Ed25519
and checked against the key pinned at join time (also its organization, version, expiry and
issue date, with five minutes of clock tolerance); the accepted policy is kept in the encrypted
vault. ThesecretsMCP server andzn runapply it. The organization can always restrict a
secret (deny, fewer hosts or commands,proxymode, where the value may go in a request) and
add custom secret formats for the scrubber; a new host for a secret applies only after the user
approves it once on this machine; it can never open a new command, mode or placement, or reach
the protections for credential files,~/.znor agent configuration. Every matching rule
restricts every agent, because zn-gate cannot prove which agent is calling; a rule's agent list
only decides who can be offered a new host. When the policy expires while offline, only its restrictions keep applying. The
audit log (metadata only, never values) is uploaded in batches that resume from the last record
the server confirmed. Join and leave are confirmed on the vault's own channel, outside the agent. zn hooks installnow starts thesecretsMCP server with--agent <id>for Claude Code, Codex,
Cursor and OpenCode.zn vault log export --otlp [--since 7d] [--out FILE]: the local audit log as OpenTelemetry logs
(OTLP/JSON Lines, oneExportLogsServiceRequestper line) for an OpenTelemetry Collector, with
no account.- Agent plugins (
plugins/zn-gate): one package in the Agent Plugins 1.0 format that
connects Claude Code and Cursor to zn-gate (hooks, thezn-gateandsecretsMCP servers and
a skill), and gives Codex the MCP servers and the skill (Codex does not run plugin hooks; use
zn hooks install codex). In Claude Code, zn-gate is installed with the plugin from its
package-lock.json; other agents use the copy you installed. On every call it requires
the exact pinned version, checks that the package holds exactly the files of the published
release, runs zn-gate from the checked bytes and fails closed otherwise. Marketplace files
for Claude Code, Codex and Cursor, and an MCP Registry entry (plugins/mcp-registry/server.json,
mcpNamein package.json). Needs Node.js 20.12 or later.
Changed
- Cloud checks (
ZN_CLOUD=1) are billed by text size, for every request and with or without windows: 1 call up to 2,000 characters, and 1 more for each further 1,000 characters or part of them, up to 19 calls for the 20,000-character limit. Before, every check counted as 1 call. Requests up to 2,000 characters are billed as before. The count comes back inbilled_callsand counts toward your plan's quota. - The project now documents how it is run: GOVERNANCE.md (decisions, releases, contributions, new maintainers), MAINTAINERS.md and an updated SECURITY.md (supported versions, how to report a vulnerability, response times and coordinated disclosure).
- GitHub Action (
action.yml):actions/setup-pythonis now v7, which runs on Node.js 24. Self-hosted runners need runner version 2.327.1 or later; GitHub-hosted runners already have it. - The
useznplugin marketplace in this repository now lists the agent plugin inplugins/zn-gate, which replaces the Claude Code plugin shipped with 1.5.x (plugins/claude/zn-gate). In Claude Code it still installs zn-gate itself: Claude Code installs the release pinned by the plugin'spackage-lock.jsoninto the plugin folder, and the launcher checks that copy file by file against the published release before running it; other agents use a copy you install (npm install -g zn-gate@1.6.0, once 1.6.0 is released; the plugin is not part of this beta). It keeps both MCP servers of the 1.5.x plugin,zn-gate(analyze_prompt,check_tool_call,check_tool_result,zn_status) andsecrets, and starts thezn-gateone with local rules only; its skill is nowzn-secrets. To upgrade in Claude Code, update the marketplace and the plugin (claude plugin marketplace update usezn, thenclaude plugin update zn-gate@usezn) and start a new session.plugins/tools/pin-release.jsnow also writes the plugin'spackage.jsonandpackage-lock.json.
Security
- Prompt hooks (all agents) block a prompt that assigns a value to a credential name anywhere in the text, also in the middle of a sentence and even when the value does not look random:
my key is STRIPE_KEY=...,DB_PASSWORD=...,"api_key": "...",apiKey=...,?api_key=...in a URL. The value must be at least 8 characters. Left alone: placeholders and demo values (changeme123,your_api_key_here,<key>,xxxx,dummy…,mock…), references (${API_KEY},$API_KEY,process.env.X,secrets.X,__ZN_…__markers), code (calls, indexes, type annotations, identifier chains such asself.api_keyornew_password) and lower-case prose such astoken: required. The block message still suggestszn secret set NAME. Tool calls and tool output do not use this rule. The Python package has no prompt hook, so it is not affected. zn hooks install claude-codeand the Claude Code plugin setonFailure: "block"on the prompt hook, so a prompt is blocked, not sent unchecked, if the hook cannot start or times out (Claude Code 2.1.295 and later; earlier versions ignore the field). Tool hooks keep the documented behaviour. docs/compat.md now lists what each agent does when the zn hook cannot run.zn-gate scan(Node and Python) reports a private key that has a BEGIN marker but no END marker (cut short when pasted or truncated), and never prints its lines: the lines after the marker that look like base64 or PEM headers count as key material, up to the first empty or other line (the empty line after theProc-Type/DEK-Infoheaders of an encrypted key is kept). Both scanners now also scan private key files without an extension (id_rsa,id_ed25519…), and skip.keyfiles with a NUL byte in the first 8 KiB (a Keynote document, not a key). The Python scanner matches extensions regardless of case (SERVER.PEM) and, like the Node one, skips files of 2 MiB or more.zn-gate scan(Node and Python) never prints a line that is inside a private key spanning several lines, even when another rule fires on that line (for example injection text next to key material); such lines show[REDACTED_PRIVATE_KEY]..pemand.keyfiles are now scanned. The printed BEGIN marker is cut to 100 characters.- Node
zn-gate scannow reports exposed credentials, as the Python scanner does: before, it reported only lines the injection rules blocked, so a file with an API key or a private key passed. Each secret is reported once per rule and line with the line redacted, and a private key whose BEGIN and END markers are on different lines is reported at its BEGIN line, showing only that marker. Files that contain credentials now make the scan exit with code 1. - Python
zn-gate scanreports a private key whose-----BEGIN ... PRIVATE KEY-----and-----END ... PRIVATE KEY-----lines are on different lines, as PEM files always have them. The scan read one line at a time, so these keys were never reported. Only the-----BEGIN ... PRIVATE KEY-----marker is shown, never the key. - Windows: looking up a program on PATH...