Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- **`diff` classifies a large base tree about eight times faster.** Translating each boundary glob into a regular expression was repeated for every path in the tree and was 74–86% of first-run `diff` time on two large repositories (#698's measurement). The compiled pattern is now cached per pattern; which paths match is unchanged, and a test checks the cached matcher against the previous implementation on over 10,000 random pattern and path pairs. (#698)
- **A hook row names a change to its `args` or to a background setting.** Changing only a command hook's exec-form arguments (`command: "python3", args: ["guard-readonly.py"]` → `["guard-write.py"]`), or only `"async": true` to `"asyncRewake": true`, was a `changed` row reading `no difference in the matcher, command or timeout; the change is in a detail this output does not show`. Each handler now publishes its `args` as a script path and a digest (host-grants `0.9`, extended in place): the first argument shaped as a relative script path that no redaction rule rewrites, alone or after the arguments before it, and a digest of the rest, as an MCP server's arguments are published, so no flag, value, absolute path or URL is shown. It also publishes the documented `type`, `async`, `asyncRewake`, `shell` and `once` settings it declares. The rows read `PreToolUse: args script guard-readonly.py → guard-write.py`, `args changed (sha256:… → sha256:…)` and `PostToolUse: async true → (none); asyncRewake (none) → true` on `diff` (text and `--json`), `verify`, the PR comment and `check`, and an added hook's cell lists them. The direction stays unknown, no script effect is inferred, and the fields are left out of grant equality, the digests and saved baselines, so no row, severity, expansion signal, loading basis or `check` decision moves, and rotating a value the digest already redacts is still no row. A change confined to `if`, `statusMessage` or another unpublished field still says it is not shown, and now names them. See [STABILITY.md](STABILITY.md#hook-args-and-settings-972). (#972, #971)
- **A hook row describes an inline command's structure, or says the digest moved and the config has to be opened.** Replaying CirrusRedOrg/EntityFrameworkCore.Jet#303, an edited `PreToolUse` script read `command changed (<not-shown> sha256:f23acba4b10f → <not-shown> sha256:3f1dc36980b7)`: two digests, and no name because the script opens with an assignment. Each handler's `command` now also publishes a `shape` (host-grants `0.9`, extended in place), read by a bounded, static reader that never runs the command: the plain-token programs at its command positions, the counts of commands, pipes, `$( … )` substitutions, `if`/`while`/`for` keywords and quoted strings, the redirects to a file (the target only for `/dev/null` or a repository-relative path), and a script path published as `args` publish one. A command the reader refuses whole (a here-document, a backquote, `case`, a function, an unterminated quote, more than 8,192 characters, or a `shell` other than `bash`) is `shape_limit` instead. The Jet row now reads `…; same programs (cat, printf, sed, grep, echo, true); simple commands 15 → 25; pipes 5 → 9; conditionals and loops 4 → 7; quoted strings 18 → 32)`; an added pipeline stage reads `programs +sh; pipes 1 → 2`, an added redirect `redirects +>> logs/x.log`, a moved script `script a.py → b.py`; an edit that keeps the structure says `same programs and structure; the change is in an argument or in quoted text this output does not show, open the config to read the change`, and an undescribed command `not described: head command uses shell syntax this output does not describe; the digest moved, open the config to read the change`. `diff` (text and `--json`), `verify`, the PR comment and `check` print the same entry. No argument, quoted string, URL, credential or absolute path is published (every shape #987 withholds stays withheld, and its rotations stay quiet), a program name is never inferred to widen or narrow anything, and the shape is left out of grant equality, the digests and saved baselines, so no row, severity, direction, expansion signal or `check` decision moves and the row still says `hook edit; authority direction is unknown`. See [STABILITY.md](STABILITY.md#hook-command-shape-934). (#934)
- **Two MCP rows say only what the read declaration establishes.** Replaying bulaofen0036-coder/TIA_Portal_Openness_MCP#46, a removed `.mcp.json` whose one server moved, unchanged, inline into `.claude-plugin/plugin.json`'s `mcpServers` (a member this entry does not read, already named under `What this run established` since #821) was a `removed` row saying "an MCP tool surface is no longer offered to the agent". Such a row now reads "the server is no longer declared in this source; whether it is still offered through a changed declaration this entry does not read is not established" when the same comparison names a changed, unread input that may declare MCP servers in the head — a plugin manifest's `mcpServers`, a manifest that does not parse, a plugin's `mcp.json` — whose plugin directory holds the removed file and whose item names the server's host (or the server's host is `unknown`). The input is never read, matched by server name or paired with the row, and keeps its coverage item; any other removal keeps its wording (#929). Replaying webiny/webiny-js#5760, `npx -y @webiny/stdlib serve` was noted `launch source is mutable`, though npm documents that a package named with no version specifier runs the local project's dependency of that name when there is one and is otherwise installed from the registry into the npm cache. An `npx` package with no version specifier is now noted `package spec has no exact version; launch resolution not established: npx may resolve a local project dependency or fall back to the registry/cache`, and its grant's `launch_source` adds `resolution: local_project_or_registry` beside `pin: mutable` (host-grants `0.9`, unreleased, extended in place; display-only, out of grant equality, digests and saved baselines). No `package.json`, lockfile or cache is read; `@latest`, ranges, tags, exact versions and other launchers keep #825's notes (#933). `diff` text and `--json`, `verify`, the PR comment and both `check` routes agree; no row, direction, `expands`, severity, expansion signal or `check` decision moves. `check` now lists the change's files only when an MCP server is removed, and names none of them. The README and quickstart change example is re-captured from the source tree and labelled not yet released. See the `MCP rows bounded to the read declaration` migration note in `STABILITY.md`. (#929, #933)
- **A hook no tool call can trigger is no longer counted as a widening.** A Claude Code `PreToolUse` group with `matcher: "Bash(git push*)"` was a `⚠ high added` row and one widening, though Claude Code compares a tool event's matcher with the tool's name and reads this one as a regular expression no tool name can match (a permission-rule pattern belongs in a handler's `if` field). Each handler of a Claude Code tool event now publishes `matcher_reach` (host-grants `0.9`, extended in place), decided by a bounded parser that never runs the matcher and claims `no_tool_name` only when every match needs a character no built-in or MCP tool name holds. Such a hook stays a row, without `⚠` or an expansion signal, and its `why` names the matcher; any matcher that may match a tool name or that the reader does not decide (`Bash`, `.*`, `mcp__.*`, `Ba.*`, `Bash(git:*)`, an unparsable pattern) keeps the `1.2.0` reading, and a nonmatching matcher edited to match is now a widening. `diff`, `verify`, the PR comment, `check`'s rows, drift and `preflight` agree; `check`'s decision does not change. See [STABILITY.md](STABILITY.md#hook-matcher-reach-940). (#940)
- **A long plain instruction document no longer hides the rest of a host comparison.** saleor/storefront#1252 adds two MCP declarations launching `next-devtools-mcp@latest` and regenerates a 346,349-byte `skills/saleor-paper-storefront/AGENTS.md`, past the instruction classifier's 256 KiB bound on both sides; `diff`, `verify` and the PR comment refused with no row and called the file only `unsupported in base and head`, which read as an unchanged file (an unchanged one has been named in `unchanged_limits` since #721; this one changed). An `AGENTS.md`, `AGENTS.override.md` or `CLAUDE.md` both sides read directly, refused only by that bound, is now left uncompared on both sides as its own `scope` of a `partial` comparison (#808's status), and the rest is compared: that profile publishes no grant, so no compared row depends on its text. A skill, command or Cursor rule past the bound, an unread role, a NUL byte, a one-sided or linked document, one a hook runs, any other changed limit and a change where nothing else was read still refuse, and an unchanged document is still a `comparable` result's unchanged limit. A blocking limit both sides carry now says whether its file changed in this change, is byte-identical, or neither is established, in its coverage `detail` and its line. `check`, `verify`'s control and the control envelope are unchanged; the classifier is unchanged. No schema version moves. See the `long plain instruction document` migration note in `STABILITY.md`. (#973)
Expand Down
64 changes: 64 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,70 @@ to mean a hook script should read `limit` (`unreadable` for a script,

---

<a id="hook-command-shape-934"></a>

## Migration Note: Unreleased — a hook row describes an inline command's structure, or says to open the config (host-grants `0.9`, #934)

**What was wrong.** Published `1.2.0` showed an inline hook command as the name
of its first word and a digest. CirrusRedOrg/EntityFrameworkCore.Jet#303 changed
one `PreToolUse` command and the row read `command changed (<not-shown>
sha256:f23acba4b10f → <not-shown> sha256:3f1dc36980b7)`: two digests, and no
name because the script opens with an assignment. A reviewer could not tell
whether the edit added a program, a branch or only text, and no row
distinguished an edit that explains itself from one that needs the file open.

**What changes.** See [hook command shape](docs/host-boundary-support.md#hook-command-shape).

- Host-grants inventory `0.9`, unpublished before this release, is extended in
place. A handler's `command` carries `shape` or `shape_limit`. `shape` is
read by a bounded, static reader of the command as `config_sha256`'s input
holds it, never run: the plain-token program names at command positions
(`commands`, at most 12, with `commands_more` and `unnamed` counting the
rest), the counts of `statements`, `pipes`, `substitutions`,
`control_flow` keywords and `quoted` strings, the `redirects` to a file as
`> target` (at most 8; the target only when it is `/dev/null` or a
repository-relative path), and a `script` path published as `args` publish
one (#972). `shape_limit` says why there is none: `too_long` (more than 8,192
characters), `unsupported_shell` (a `shell` setting other than `bash` or
`sh`) or `unsupported_syntax` (a here-document, backquote, `$(( … ))`,
`case`, function definition, unterminated quote or group, or a command past
the reader's word or nesting bound). No argument, quoted string, variable,
URL or absolute path is published, and a token that looks generated is not
named.
- A changed command's row names what the shapes differ in: `command changed
(<not-shown> sha256:f23acba4b10f → <not-shown> sha256:3f1dc36980b7; same
programs (cat, printf, sed, grep, echo, true); simple commands 15 → 25; pipes
5 → 9; conditionals and loops 4 → 7; quoted strings 18 → 32)` for the Jet
pair, `programs +sh; pipes 1 → 2` for an added pipeline stage, `redirects
+>> logs/x.log`, `script a.py → b.py`. When the shapes are the same it says
`same programs and structure; the change is in an argument or in quoted text
this output does not show, open the config to read the change`, and when
either command is not described, `not described: head command uses shell
syntax this output does not describe; the digest moved, open the config to
read the change`. An added or removed hook's cell lists what a command that
is more than one program and its arguments is made of (`runs cat, jq, sh; 2
pipes; 1 redirect (>> logs/x.log)`).
- `diff` (text and `--json`), `verify`'s host comparison, the PR comment and
`check`'s rows print the same entry.

**Who must act.** Nobody. A reader that matched the exact `command changed
(…)` text, or that validates `0.9` handlers against a schema generated before
this change, updates to the new text and schema.

**What does not change.** The shape is a function of the command as
`config_sha256`'s input holds it and, like the rest of `handlers`, is left out
of grant equality, the inventory digests and saved baselines, so no row, row
value, direction, `expands`, severity, expansion signal, loading basis (#714),
`matcher_reach` (#940) or `check` decision moves, and a hook edit stays `hook
edit; authority direction is unknown`: a shape names programs and counts, never
what a command does, whether a host runs it or which way an edit moves
authority. A value the digest's input redacts is withheld from the shape too,
so rotating one is still no row (#987). No schema file present in a tagged
release changes. No check ID, CLI flag, control state or permission is added.
`minimum_control_contract_version` stays `21`.

---

<a id="hook-args-and-settings-972"></a>

## Migration Note: Unreleased — a hook row names its `args` and settings (host-grants `0.9`, #972, #971)
Expand Down
2 changes: 1 addition & 1 deletion docs/distribution-surfaces.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions docs/host-boundary-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,61 @@ see #987) is still no row. A value no rule redacts, such as a bare positional
token, or a password split from its `-u` into the next argument, moves the
digest, so its rotation is a row of digests that prints neither value.

<a id="hook-command-shape"></a>

An inline hook command is described by its structure, not its text (host-grants
`0.9`, #934). A command that opens with an assignment, pipes through `sed` or
branches on `grep` has no executable name to show, and a digest alone told a
reviewer neither whether the edit added a program nor whether it needs the file
open. Each handler's `command` now carries `shape`, read from the command as
`config_sha256`'s input holds it by a bounded, static reader that never runs it.
The reader accepts simple commands; `|`, `|&`, `&&`, `||`, `;`, `&` and
newlines; `( … )` and `{ … }` groups; `if`/`elif`/`then`/`else`/`fi`,
`while`/`until`/`for`/`do`/`done`, `!` and `[[ … ]]`; `NAME=value` prefixes;
`$( … )` substitutions; quoting; the redirects `>`, `>>`, `<`, `2>&1` and `&>`;
and a literal `bash -c '…'` script, read inside to two levels. It publishes
`commands`, the programs named at command positions in order (at most 12; each
a plain token that no redaction rule rewrites, never a URL or a token that
looks generated; `commands_more` counts the rest and `unnamed` the command
positions whose word is a variable, a substitution, a quoted or glob word);
`statements`, `pipes`, `substitutions`, `control_flow` (the `if`, `elif`,
`while`, `until` and `for` keywords) and `quoted` (quoted strings), as counts;
`redirects`, each `> target`, `>> target` or `< target` (at most 8), the target
only when it is `/dev/null` or a repository-relative path of letters, digits,
`.`, `_` and `-` with no `..` segment that no redaction rule rewrites, and
`<not-shown>` otherwise; and `script`, the path of a command word shaped as a
relative script path, or of the first such argument of an interpreter such as
`python3`, `node` or `bash`, by the rule `args` use (#972). It follows no
wrapper (`env`, `sudo`, `xargs`) and no alias, so a wrapper is named and what it
runs is not. No argument, quoted string, variable, URL or absolute path is
published, and a backslash-continued line is read joined, so a value the string
rule leaves on the next line is withheld too.

The reader refuses what it does not read, whole: a here-document or
here-string, a backquote, `$(( … ))`, `$'…'`, a process substitution, `case`,
`select`, `coproc`, a function definition, an array assignment, an unterminated
quote or group, nesting past 8 levels, more than 2,000 words, or more than
8,192 characters. The handler then carries `shape_limit` (`unsupported_syntax`
or `too_long`) instead, and a `shell` setting other than `bash` or `sh` is
`unsupported_shell`. A changed command's row names how the shapes differ —
`command changed (<not-shown> sha256:f23acba4b10f → <not-shown>
sha256:3f1dc36980b7; same programs (cat, printf, sed, grep, echo, true); simple
commands 15 → 25; pipes 5 → 9; conditionals and loops 4 → 7; quoted strings
18 → 32)`, `programs +sh; pipes 1 → 2`, `redirects +>> logs/x.log`, `script
a.py → b.py` — and says so when they do not: `same programs and structure; the
change is in an argument or in quoted text this output does not show, open the
config to read the change`, or, when either command is not described, `not
described: head command uses shell syntax this output does not describe; the
digest moved, open the config to read the change`. One program replaced by
another is left to the two executables. An added or removed hook's cell lists
what a command of more than one program is made of (`runs cat, jq, sh; 2 pipes;
1 redirect (>> logs/x.log)`) and names a script path beside a single program.
A shape claims nothing about what a command does, whether a host runs it, or
which way an edit moves authority: the row's direction stays unknown. Like the
rest of `handlers` it is left out of grant equality, the inventory digests and
saved baselines, and it is a function of the same text the digest is, so a
value that text redacts (#987) moves neither, and rotating one is still no row.

<a id="hook-matcher-reach"></a>

A Claude Code hook on a tool event — `PreToolUse`, `PostToolUse`,
Expand Down
Loading
Loading