Skip to content

v0.25.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 05:22
· 187 commits to main since this release
23b21ab

Install: npm i -g @stroq/cli@0.25.0, or npx @stroq/cli@latest init. Every change, with the reasons, is in the changelog.

Added

  • stroq prove checks that the hook really stops things in your agent. A hook written into an agent's settings says nothing about whether the agent obeys it. stroq prove asks Claude Code itself, claude -p, for up to three short Haiku requests with your own login (subscription usage; an API key is never used, and the check kills the host at once if it sees one): one harmless command the policy allows, one the policy denies (a git config setting that makes git run a program, written to a file of its own, nothing is installed), and the denied one again with the hook out of the way, to show that the host would have run it. A host is verified only when the allowed one ran, the denied one was stopped in the hook's own words and left no file, and the same command did run without the hook; a model that declines, a limit, a host that stops the control by its own rules, or a missing billing field is inconclusive and never verified or failed. A third probe (a curl with a made-up key) is optional: the model itself usually declines it. Everything runs in a temporary folder that is removed, also on Ctrl-C. Measured on Claude Code 2.1.271: verified in exactly three requests. stroq doctor shows the result as proof: and marks it stale when Stroq, the policy or the host version changes. It shows that the hook line stroq init writes works with that host version; it does not show that your own settings file is loaded, and not that other versions or hosts behave the same. Claude Code only for now; --dry-run shows what it would do, --json prints the result, --max-requests raises the cap (at most 6). An agent cannot run it.
  • stroq vet --from <dir> reads a skill, MCP server or plugin before you add it. It reads the folder without following a link or running anything, and prints its passport: what it asks for or can do (the tools its front matter allows, its hooks, the hosts its shell text and code reach, the environment variables and credential files it reads, the programs it runs, install scripts, dependencies that do not come from a registry, native or obfuscated code, symlinks that leave the folder), each marked [declared] or [observed] with the file and line, sensitive ones first, then the signals and every blind spot (what was not looked at: nothing was run, what it fetches at run time, other languages, prose). The name is the source's where the source names it; a package that calls itself something else gets a declared-name-differs signal. A reading that fails is named and the command exits 1, so an artifact cannot hide a finding by breaking a reader. --json prints the canonical passport (the same bytes for the same files, to commit), --expect <digest> checks the stroq-tree/1 digest (not on Windows, where a folder has no executable bit), --card prints a Markdown snippet for a README, --out <file> writes to a new file. It is a reading of the files, not a security certificate, and it says so. stroq install now says that Stroq has no install and points to init and vet, instead of suggesting uninstall. Not yet: --online (npm, GitHub), archives, add/remove.
  • stroq policy check and stroq policy test show what an edit of your policy does before you use it. check reads the file exactly as the hook does and names every fault by its field (a policy that does not load makes the hook fail closed, and it says so), then lints it: a rule that an earlier one always decides first, a protection of the default policy that this one quietly loosens (each of the 20 action classes alone, in a clean and a tainted session: secret.egress going from deny to allow is an error), a threshold far above the default's, and a field Stroq does not read (a typo such as taintt: silently leaves a rule matching everything). test replays the 36 attack scenarios and the 75 scenarios of ordinary work under your policy and under a baseline (the default unless you name one), and prints every step whose decision changed (<scenario>/<step>: deny -> allow (looser)), the attacks that now pass, the ordinary work it now interrupts, and the expectations of your own cases (--cases, YAML or JSON) that it breaks; it exits 1 on any of those, on a result that could not be judged, or past --max-new-interruptions. Offline and in-process; --json for both.
  • stroq harden makes the host refuse to read your credential files, and takes it back. stroq harden shows what it would change and writes nothing; stroq harden apply asks you (on a terminal; elsewhere it needs --yes) and writes it through the reversible edit engine, with a copy of the file; stroq harden undo gives the file back and keeps anything you changed since; stroq harden status says what is still in place. The first recipe adds Read(...) rules for the usual credential files (.env, ~/.ssh, ~/.aws, ~/.config/gcloud, ~/.kube, ~/.docker/config.json, ~/.npmrc, ~/.netrc, ~/.pgpass, ~/.git-credentials, ~/.gnupg) to permissions.deny of Claude Code's settings (--scope user, or --scope project for .claude/settings.local.json). Measured on Claude Code 2.1.271: the Read tool and a plain cat through Bash are refused. It is a filter inside the host and a best effort, not a guarantee: a program that reads the file itself (python -c) is not covered, and only stroq run --sandbox is. A second recipe writes a marked block into .cursorignore for Cursor; it could not be tested here and says so. A settings file with comments is not rewritten: the command prints the lines to add by hand. An agent cannot run harden apply, harden undo, or harden --acknowledge-link.

Changed

  • .cursorignore is protected like the other files that switch a protection off. An agent can no longer write it (config.self), because stroq harden puts a block in it that keeps .env and keys out of what Cursor sends to a model.

Fixed

  • The live-check library reads a stored result only from the file of its kind. Still a library: no command uses it. A stand-in result moved into the live file is no result, a result dated more than a few minutes ahead of the clock is none either (with a clock set back it would have outlived every install), and the environment the driver is handed no longer carries STROQ_* or CLAUDE_CONFIG_DIR, which the model's shell inherits (a ledger path it could delete, a home that sends the hook to the real settings). The store says that its file is tamper-evident and not tamper-proof.
  • The task-scope library is closed where a review found it open. Still a library: no command uses it. A task id with no permit file is a deleted contract and refuses every tool (it was "not a task", so an agent that could delete the file turned the task off); an address in a command is a network operation whatever class the command got (git fetch URL, git clone, npm install URL passed a no-network scope), and --connect-to, --resolve and the proxy options are refused under a scope that lists hosts; Edit, MultiEdit and NotebookEdit list a read as well as a write, so a read deny holds against them; a tool the classifier read no resources of (Task, NotebookRead, LSP) is held as an incomplete call; the approval of an ask names the whole call, content included, and the hint names the first operation still lacking one; the refusal text is redacted in the audit; an MCP name with __ in the tool is not granted to the server before it; the key store writes its files whole and refuses a key directory that is a link.
  • The Safe Install contract refuses more names that are one name on some disk or another. Still a library: no command uses it. Found by a review of 0.24.0. A path component that a program which narrows names would turn into a separator (U+FF0F, U+FF3C), any ~ and a digit (an NTFS short name), a look-alike space, and the combining mark U+0345 that NTFS upper-cases are refused; a symlink target may not hold a control, an invisible character or U+FFFD; the parts of an npm or GitHub source have the lengths npm and GitHub allow; buildTree takes { complete: true } to refuse an entry with no content, and no longer believes the length a byte array reports of itself; schemaFault describes a record's faults without quoting its text.