Skip to content

zn-gate 1.13.0

Latest

Choose a tag to compare

@github-actions github-actions released this 19 Oct 14:48
· 10 commits to main since this release
1174db0

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 connect turns on the optional cloud check for analyze_prompt from 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. connect accepts 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 status shows 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 disconnect removes 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 connection connect uses. 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-revoke removes only the local copy. A failed revoke tells you and links https://usezn.com/api-keys. In your own terminal, --revoke skips the question and --no-revoke never revokes. disconnect only 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 report shows what zn-gate did on this computer in the last 7 days (--since 1d-90d for another period, --json for 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-line hides 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, mode and format inputs, a guide for the audit mode (docs/github-action.md) and a minimal audit example. No change in behaviour.
  • zn-gate check now tells you when the Claude Code hooks installed by an older zn-gate lack the check for typed skills and commands, and suggests running zn hooks install claude-code again. It says nothing for the plugin, which already has it.
  • The connect help 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 check and connect guides gained the same kind of list.
  • zn-gate init no 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, and zn-gate check reports an installed plugin on such a Node.js with the way out.
  • The docs say what zn-gate status shows: 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 reach and zn 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 version input 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 on usezn/zn@v1.12.0 without version installed zn-gate 1.10.0. Set version in 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, by evidence --verify (non-zero exit), evidence and evidence --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 Windows cmd move %USERPROFILE%\.zn\evidence.jsonl %USERPROFILE%\.zn\evidence.damaged.jsonl, in PowerShell Move-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 a case close inside a group opened on an earlier line cannot hide one. Bodies a shell, an interpreter, xargs or parallel runs, a program with an option that runs code (sort, split), a command that redefines a function or PATH, and a file the same command writes and then runs (also by an absolute path, also behind nohup, timeout or exec), are still refused. Every read of .env or 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\keychain paths and paths with ./ segments, such as zn-gate/./lib/client.
  • zn-gate reach --remember shows 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: Bearer value made mostly of one repeated character (for example sk-ant-api03-xxxx...) is treated as a placeholder. SCRUB_RULES_VERSION is 2026-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-gate calls 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-keys or acme-cloud-key-manager for zn's own cloud-key module.

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 audit hides 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 \n between the flag and its value.
  • Claude Code: zn now also checks what a typed skill or command expands into. A new UserPromptExpansion hook 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 .env file. If the hook cannot run, the expansion is blocked. Run zn-gate hooks install claude-code again to add it; the plugin has it already. To turn off ! shell commands in skills altogether, set disableSkillShellExecution: true in 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-StoredCredential with zn's service or target name), and also security dump-keychain and 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 by zn-gate connect or 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 to https://api.usezn.com: --url and ZN_API_URL are ignored for it.
  • The optional cloud check now turns on only with your own opt-in. A key in ZN_API_KEY still does nothing without ZN_CLOUD=1, even if the record zn-gate connect leaves in ~/.zn/config.json was written by hand: when that record is what turns the check on, zn-gate uses only the key connect saved, 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 connect and zn-gate disconnect now count as commands only the user may run. POLICY_RULES_VERSION is 2026-10-18.31.
  • The saved cloud key is also held back while NODE_OPTIONS has --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 or zn 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 their zn-gate/lib/client path) to ask for the key, as a barrier only.
  • The saved cloud key is not used while NODE_DEBUG is set (Node can print request headers, key included): connect, disconnect and the cloud check say so in one line and send nothing. NODE_OPTIONS options are also recognised when written with underscores (--tls_keylog, --inspect_brk, --experimental_loader), which Node accepts, in the checks of connect and zn audit. AUDIT_RULES_VERSION is 2026-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 after cd is looked up in the folder the cd leads to (an unknown cd refuses 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 .pub files stay readable from the disk.
  • The agent hooks: git is recognised behind sudo, env, command, nohup and xargs, 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 as bash -O extglob -c and --rcfile, for fish -C, for PowerShell parameters given as any prefix or with / (-en, /Command), for powershell "<code>", and for iex and Invoke-Expression (also when a string is piped into them), and sg -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 --remember keeps 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 -c of tools such as psql or grep is 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, so evidence --verify reported a break that was not tampering and a real break could be hidden among them. Every write now takes a short lock first (an evidence.jsonl.lock file 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_hash that 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 .env file 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 .env name is now recognised after a backslash separator on every platform.
  • More automated tests read a .env with 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 .env with 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