- M2: stop echoing the matched pattern name in the user-visible
[BLOCKED: ...] message returned by the firewall. The pattern (and
the matched secret class) still appear in the operator log, but the
string sent back into the prompt is now generic.
- H1: document Rule.Pattern semantics on the Rule type and pin them
with a regression test. Pattern is a case-sensitive, exact substring
match against the JSON-serialised tool arguments — not a glob,
regex, or whitespace-insensitive match. The new test exercises both
matches and the documented gotchas (double-space, case drift, tab).
- H3: every code path in CommandExecutor.Execute that converts a hook
failure into Allow via FailOpen now emits a WARN naming the hook
and the failure mode (timeout / launch_error / parse_error), so
chronic hook failure or abuse is visible in operator logs.
Also tightens errcheck on permission/rule.go (Printer.Print on a
strings.Builder cannot error in practice; make the intent explicit).
Plugin loader resolves HookSpec.Exec as a relative path joined to the
plugin directory, and manifest.checkSafePath rejects absolute paths and
'..' traversal — Exec was always meant to be an executable path.
The hook executor was wrapping it in 'sh -c', adding a redundant shell
interpretation step that turned any space, quote, or metacharacter in
the path into command-injection surface. Switch to exec.Command(path)
with no shell wrapping.
Closes audit finding C3. Adds a regression test that fails under the
old 'sh -c' code path: a canary file created via shell sequencing
remains absent when the executor treats Exec as a literal filename.
Hook command tests now write small /bin/sh scripts to t.TempDir and
point Exec at those — matching production semantics (resolved binary
path) rather than inline shell strings.