Repository navigation
zn-gate 1.13.0 adds zn-gate connect, which turns on the optional cloud check for analyze_prompt with an analysis-only API key kept in your system keychain, and zn-gate disconnect, which can also revoke that key on usezn.com. It also adds zn-gate report, a local summary of what zn-gate did in the last days, and the agent hooks now check what a typed skill or command expands into and the code that shells and wrappers run from a string. zn-gate still needs no account and sends nothing by default. This release also fixes how a damaged audit log is handled (it is reported instead of crashing, and never used to start a new chain) and how several programs writing to it at once keep its hash chain intact and tightens when the saved cloud key can be used. Update with npm install -g zn-gate@1.13.0 and pip install -U zn-gate.
Added
zn-gate connectturns on the optional cloud check foranalyze_promptfrom your own terminal. It opens usezn.com, you paste an analysis-only API key at a hidden prompt, and the key is kept in your system keychain or the zn vault, never in a file.connectaccepts only a key limited to analyzing text, because such a key cannot read your usage or manage keys, so a leaked copy can do little; a full-access key is refused.zn-gate statusshows whether a key is connected and where it comes from, without showing it. Nothing changes until you run it: zn-gate still needs no account and sends nothing by default. See docs/connect.md.zn-gate disconnectremoves the saved key and can also revoke it on usezn.com, so it stops working everywhere. It asks first (the default is no). Only an explicit yes sends anything: one request with the key to https://api.usezn.com, over the same checked connectionconnectuses. Once you have answered, the local copy is removed even if the revoke request fails; if the saved key cannot be read, nothing is removed and--no-revokeremoves only the local copy. A failed revoke tells you and links https://usezn.com/api-keys. In your own terminal,--revokeskips the question and--no-revokenever revokes.disconnectonly runs in your own terminal: from a script or an AI agent it refuses and changes nothing, with or without those options. zn-gate still needs no account and sends nothing by default. See docs/connect.md.- A zn API key (
zn_followed by 48 characters) is now recognised by the scrubber and hidden like other provider keys. zn-gate reportshows what zn-gate did on this computer in the last 7 days (--since 1d-90dfor another period,--jsonfor scripts): blocked actions by agent, secrets hidden by type, new protected MCP servers, tool changes waiting for your approval and the busiest day. It reads only local files, writes nothing and sends nothing. It ends with one line about zn Team, which--no-team-linehides and which does not appear once the computer has joined a Team. See docs/report.md.
Changed
- GitHub Action: a new name and description for the GitHub Marketplace listing, clearer descriptions of the
path,modeandformatinputs, a guide for the audit mode (docs/github-action.md) and a minimal audit example. No change in behaviour. zn-gate checknow tells you when the Claude Code hooks installed by an older zn-gate lack the check for typed skills and commands, and suggests runningzn hooks install claude-codeagain. It says nothing for the plugin, which already has it.- The
connecthelp now says it takes an analysis-only API key. - Added a glossary and "If something fails" lists to the secrets, reach, audit and Python package guides.
- The install steps in the README and the npm package README are easier to follow, with a short "If something fails" list. The
checkandconnectguides gained the same kind of list. zn-gate initno longer installs the Claude Code plugin when Node.js is older than 20.12, which the plugin needs: it sets up zn's hooks for Claude Code instead and says so in one line, andzn-gate checkreports an installed plugin on such a Node.js with the way out.- The docs say what
zn-gate statusshows: the detection engine and whether the optional cloud check is on. Sharing of usage statistics has its own status,zn-gate stats share status. - More automated tests cover
zn team(the commands, offline and revoked machines, expired policies) and the organization's custom secret formats. No change in behaviour. - More tests for the MCP servers and
zn run(malformed messages, unknown methods, missing parameters, vault errors). No change in behaviour. - More tests for the Python package scan command, evidence ledger and local stats (binary files, other encodings, size limits, empty folders, damaged ledgers). No change in behaviour.
- More tests for the Python SDK guard decorator and cloud client (network errors, timeouts, empty and malformed responses). No change in behaviour.
- More automated tests cover
zn-gate reachandzn audit(agent settings, SARIF output and the reach memory file). No change in behaviour. - More tests cover the vault's encrypted store, keychain metadata, audit log and confirmation prompts. No change in behaviour.
- The command line output of the Node.js and Python packages has no color codes when NO_COLOR is set to any non-empty value, when TERM=dumb, or when the output is not a terminal. With NO_COLOR or TERM=dumb it also leaves out symbols and box-drawing lines. The status words (ALLOWED, BLOCKED, FAILED, PASSED, ALLOW, BLOCK, clean) keep the meaning. Exit codes and the JSON, SARIF and agent hook outputs do not change.
Fixed
- GitHub Action: the
versioninput now defaults to the version of the Action release itself (1.13.0 here). The 1.12.0 release of the Action defaulted to 1.10.0, so a workflow onusezn/zn@v1.12.0withoutversioninstalled zn-gate 1.10.0. Setversionin your workflow to choose exactly what runs. - The audit log (
~/.zn/evidence.jsonl) is now checked for damage before anything is added to it. A log with unreadable bytes or a cut-off last line is reported as corrupted, with the line number, byevidence --verify(non-zero exit),evidenceandevidence --export, instead of failing with a Python error. A damaged log is never used to start a new chain: the event is not written, a warning is shown (the first time, then every 5 minutes), and your command or agent carries on as usual. To recover, move the damaged file aside (mv ~/.zn/evidence.jsonl ~/.zn/evidence.damaged.jsonl; on Windowscmdmove %USERPROFILE%\.zn\evidence.jsonl %USERPROFILE%\.zn\evidence.damaged.jsonl, in PowerShellMove-Item $env:USERPROFILE\.zn\evidence.jsonl $env:USERPROFILE\.zn\evidence.damaged.jsonl): the next event starts a new chain and the old file stays for review. An empty or missing log still starts a fresh chain. The Node and Python versions now read the log the same way, so a log with a byte-order mark or non-breaking spaces is reported the same by both. - The hook no longer refuses a command that writes a file from a here-document (
cat > file <<'EOF') when the body holds a*or another glob character, and it checks a 100 KB body in a few milliseconds instead of tens of milliseconds. - A here-document body is only read as data in a plain simple data command (
cat,tee,dd, ...) with no pipe, process substitution or open group (also one opened on an earlier line). A pipe or a process substitution after the first body keeps the check too, so acaseclose inside a group opened on an earlier line cannot hide one. Bodies a shell, an interpreter,xargsorparallelruns, a program with an option that runs code (sort,split), a command that redefines a function orPATH, and a file the same command writes and then runs (also by an absolute path, also behindnohup,timeoutorexec), are still refused. Every read of.envor a credential file on the command line is refused as before. The check is linear with the existing time budget, so 10 000 here-document operators or 50 000 parentheses on one line stay within it. - The guard that stops a command from loading zn-gate's own key code now also catches upper-case paths, Windows-style
vault\keychainpaths and paths with./segments, such aszn-gate/./lib/client. zn-gate reach --remembershows the date of the previous run only when it is a plain date; an edited or damaged memory file can no longer put other text or terminal codes into the report.- The scrubber no longer reports a masked example as a secret: an Anthropic key or an
Authorization: Bearervalue made mostly of one repeated character (for examplesk-ant-api03-xxxx...) is treated as a placeholder.SCRUB_RULES_VERSIONis2026-10-17.3. zn audit: when it hides a secret value in escaped JSON ({\"password\":\"...\",\"user\":\"...\"}), it hides only that value and keeps the next fields, and a URL after a flag such as-url:is no longer taken for-u user:password. (Also in 1.12.0.)- Internal safety default in the vault: the function that deletes the vault key no longer touches the shared keychain entry when it has no record of which keychain this vault uses, unless the caller names one. Nothing in
zn-gatecalls it today, so there is no change for users. - A log that exists but cannot be written (no permission, full disk) no longer makes the hook or proxy fail: the event is skipped, a warning is shown (the first time, then every 5 minutes) and the decision is unaffected.
- The Node and Python versions can now share one audit log that holds non-ASCII text (for example accented letters or CJK). Before, a record written by one failed verification in the other. New records are written as plain UTF-8 text; logs written by earlier versions of either one still verify.
- The rule that protects zn's key no longer mistakes a module named
cloud-keysoracme-cloud-key-managerfor zn's owncloud-keymodule.
Security
- Some provider token formats were not redacted by the library API, the MCP tool checks, the text sent to the cloud analysis and the local evidence preview. They are now redacted the same way as in the Python package. The agent hooks were not affected.
zn audithides the value after a credential flag in an array of arguments in more cases, in its text, JSON and SARIF reports: single-quoted TOML items (args = ['--token', 'VALUE']), arrays written one item per line, quotes escaped once or twice, and a comment (also a multi-line block comment or one longer than 1024 characters), several blank lines, a very long line, a wide run of spaces or a literal\nbetween the flag and its value.- Claude Code: zn now also checks what a typed skill or command expands into. A new
UserPromptExpansionhook blocks the expansion, with the reason and the next step, when it would show the model a secret value, a private key or the lines of a.envfile. If the hook cannot run, the expansion is blocked. Runzn-gate hooks install claude-codeagain to add it; the plugin has it already. To turn off!shell commands in skills altogether, setdisableSkillShellExecution: truein Claude Code. - The hooks refuse commands that ask the system keychain for zn's own entries (
security find-generic-password,secret-tool lookup,cmdkey /list,Get-StoredCredentialwith zn's service or target name), and alsosecurity dump-keychainand code that loads the key through zn's own modules or a keyring library, so the direct ways for an agent to read the key saved byzn-gate connector the vault key are closed. A program running as you can still ask the keychain, as for any entry. The same tools stay free for other entries. A saved key is only ever sent tohttps://api.usezn.com:--urlandZN_API_URLare ignored for it. - The optional cloud check now turns on only with your own opt-in. A key in
ZN_API_KEYstill does nothing withoutZN_CLOUD=1, even if the recordzn-gate connectleaves in~/.zn/config.jsonwas written by hand: when that record is what turns the check on, zn-gate uses only the keyconnectsaved, only to https://api.usezn.com, and the record must match the saved key (its last four characters). If the key cannot be read, prompts are checked locally and zn-gate says so once. - The agent hooks: a command that reads the system keychain for zn's entries is checked in linear time (it took several seconds on a 100 KB command before), and
zn-gate connectandzn-gate disconnectnow count as commands only the user may run.POLICY_RULES_VERSIONis2026-10-18.31. - The saved cloud key is also held back while
NODE_OPTIONShas--tls-keylog,--inspect(with its variants) or--openssl-shared-config, and the zn vault's request tool for agents (secrets_http_request) never fills in that key, so an agent cannot use it to revoke the key or spend its quota. The vault does not give the key to the hooks, the secrets broker orzn run; a program running as you can still read it, as with any vault or keychain secret, and can use it to analyze text on your quota or revoke it (the key cannot read usage or manage other keys). The hooks also refuse a command that loads zn-gate's own client modules (found by theirzn-gate/lib/clientpath) to ask for the key, as a barrier only. - The saved cloud key is not used while
NODE_DEBUGis set (Node can print request headers, key included):connect,disconnectand the cloud check say so in one line and send nothing.NODE_OPTIONSoptions are also recognised when written with underscores (--tls_keylog,--inspect_brk,--experimental_loader), which Node accepts, in the checks ofconnectandzn audit.AUDIT_RULES_VERSIONis2026-10-17.1. - The agent hooks: a glob that can match a private key file (
cat certs/*.key,certs/*) is checked against the files it matches on the disk, a key file read aftercdis looked up in the folder thecdleads to (an unknowncdrefuses it, like a.env), and reading a key file from the git repository or its history (git show HEAD:certs/server.pem,git log -p -- server.key) is refused. Certificates and.pubfiles stay readable from the disk. - The agent hooks: git is recognised behind
sudo,env,command,nohupandxargs, a<rev>:<path>word naming a.env, credential or key file is refused anywhere in a command that runs git (echo HEAD:.env | xargs git show), and git pathspec magic (':(top).env',':/.env') is read as the path. - The agent hooks: code run from a string is also checked behind more wrappers (
chrt,taskset,strace,ltrace,runuser,chroot,unbuffer,nsenter,script -c) and when a wrapper is not one zn knows, after shell options such asbash -O extglob -cand--rcfile, forfish -C, for PowerShell parameters given as any prefix or with/(-en,/Command), forpowershell "<code>", and foriexandInvoke-Expression(also when a string is piped into them), andsg -c. zn-gate reach: a server, file or path name that contains a line break or a tab can no longer start a line of its own in the text report; such characters become a space. Zero-width and other invisible characters are removed from every name before secret values are hidden and again after, and the file that--rememberkeeps is ignored when it is larger than 1 MiB.- The agent hooks: the code a shell or wrapper runs from a string (
sh -c,bash -lc,sudo sh -c,su -c,flock -c,env -S,eval,watch,pwsh -Command,cmd /c,Start-Process git -ArgumentList; the-cof tools such aspsqlorgrepis not code and is left alone) is now checked as a command of its own, so every rule applies inside it, including reading a.env, credential or key file from git history. Nesting deeper than three levels is refused. - Hardened the audit log (
~/.zn/evidence.jsonl), whose hash chain is what lets you notice if a record was changed or removed. Several processes writing at once (two agents in one project, or the Node and Python versions together) could each add a record after the same previous one and split the chain, soevidence --verifyreported a break that was not tampering and a real break could be hidden among them. Every write now takes a short lock first (anevidence.jsonl.lockfile next to the log, shared by both versions), so the records stay in one line. If the lock stays busy for 2 seconds the write is skipped with a warning instead of risking a split, and a lock left behind by a crashed process is cleared after 10 seconds. A writer that was paused for a long time and lost its lock checks this before writing, so it cannot add a record based on out-of-date information. - A
record_hashthat is not 64 lowercase hex characters is now treated as a damaged last record, like other damage, instead of being trusted as the start of the next link. - The key-protection rule now also recognises the module names when they are written with escape sequences (JavaScript or shell, such as
\x2f) or split by quotes. - The agent hooks: a
.envfile reached through a Windows root, a network share or an extended-length path (type %CD%\.env,more \\server\share\.env,gc \\?\C:\path\.env) is refused, because zn cannot read a path like that to check whether the file holds values, and a.envname is now recognised after a backslash separator on every platform. - More automated tests read a
.envwith the commands of cmd.exe and Windows PowerShell (type,more,copy,findstr,Get-Content,gc,-Path,-LiteralPath,Select-String) over relative, absolute, drive-relative, profile-variable and UNC paths: a.envwith values is refused and an ordinary file is allowed in every form, on Linux and in the Windows checks job. No change beyond the refusal above.
How to verify these files: docs/verify-release.md