Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8359d61e33 | ||
|
|
400d2b9b6b | ||
|
|
4edf42ce9f | ||
|
|
2f53346b8c | ||
|
|
dcc0677270 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "skills",
|
||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||
"version": "0.19.1",
|
||||
"version": "0.19.3",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
|
||||
@@ -41,7 +41,17 @@ if [ ! -f "$CONFIG" ]; then
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENVFILE" ]; then
|
||||
# EXPECTED IN CI, and permanently so. The env-file holds live credentials, so
|
||||
# it is not on the runner and must not be -- which means this check can only
|
||||
# ever verify on a host that has one. infra#368 added jq to the runner image
|
||||
# believing that would let CI verify; it removed the jq branch below and
|
||||
# changed nothing else, because this branch is the binding one.
|
||||
#
|
||||
# Said plainly so nobody concludes CI covers this. It does not, it cannot,
|
||||
# and putting ~/.op-env on the runner to make it would be trading a real
|
||||
# secret for a green tick.
|
||||
echo "ERROR: no such env-file: $ENVFILE -- cannot verify MCP variables" >&2
|
||||
echo " (expected in CI: this check only verifies on a host with ~/.op-env)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/sh
|
||||
# Tests for hooks/pre-task-ritual.sh — the UserPromptSubmit hook that surfaces
|
||||
# rule 0 on every prompt (#44).
|
||||
#
|
||||
# WHY THIS EXISTS, AND WHAT IT DELIBERATELY DOES NOT CLAIM
|
||||
# This hook is three lines and echoes a string. It has no bash-isms, so it
|
||||
# cannot fail the way secret-guard appeared to; that risk was overstated when
|
||||
# this test was requested. What it CAN do is stop working silently, and its
|
||||
# whole job is to be visible -- if the text stops appearing, rule 0 stops being
|
||||
# surfaced on every prompt and nothing announces that.
|
||||
#
|
||||
# THE ASSERTION THAT MATTERS IS REGISTRATION.
|
||||
# A hook file that exists but is not in hooks.json does absolutely nothing, and
|
||||
# looks identical on disk to one that works. So this derives the command from
|
||||
# hooks.json rather than assuming one: the registration is the source of truth
|
||||
# for how the hook is invoked, and a test that invokes it its own way is
|
||||
# testing something the harness never runs.
|
||||
#
|
||||
# That distinction is not hypothetical. secret_guard_test.sh piped its hook into
|
||||
# `sh` while hooks.json runs it with `bash`; on Debian /bin/sh is dash, which
|
||||
# has no `set -o pipefail`, and the suite failed 20/20 in CI against a hook that
|
||||
# was fine in production. The test was wrong, not the hook. Deriving the
|
||||
# invocation removes the opportunity for that class of mistake entirely.
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
HOOK="$ROOT/hooks/pre-task-ritual.sh"
|
||||
REG="$ROOT/hooks/hooks.json"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||
no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
printf ' SKIP: jq not installed -- cannot read the hook registration\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -f "$HOOK" ] || { printf ' FAIL %s does not exist\n' "$HOOK"; exit 1; }
|
||||
ok 'hook file exists'
|
||||
|
||||
[ -f "$REG" ] || { printf ' FAIL %s does not exist\n' "$REG"; exit 1; }
|
||||
|
||||
# 1. Registered, and on the right event. UserPromptSubmit is what makes it fire
|
||||
# on every prompt; registered under any other event it would still "exist"
|
||||
# and still do nothing useful.
|
||||
CMD=$(jq -r '
|
||||
.hooks.UserPromptSubmit[]?.hooks[]?
|
||||
| select(.command | test("pre-task-ritual"))
|
||||
| .command
|
||||
' "$REG" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$CMD" ]; then
|
||||
ok 'registered on UserPromptSubmit in hooks.json'
|
||||
else
|
||||
no 'not registered on UserPromptSubmit -- the file would exist and never run'
|
||||
printf ' %d passed, %d failed\n' "$pass" "$fail"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Run it EXACTLY as registered, with the harness variable substituted the way
|
||||
# the harness substitutes it.
|
||||
OUT=$(CLAUDE_PLUGIN_ROOT="$ROOT" sh -c "$CMD" 2>&1)
|
||||
rc=$?
|
||||
|
||||
[ "$rc" -eq 0 ] && ok 'exits 0 as registered' \
|
||||
|| no "exited $rc -- a non-zero UserPromptSubmit hook can block the prompt"
|
||||
|
||||
# 3. The content is the point. A hook that runs and says nothing is the same as
|
||||
# no hook, and that is the failure this pins.
|
||||
if [ -n "$OUT" ]; then
|
||||
ok 'produces output'
|
||||
else
|
||||
no 'produced no output -- rule 0 would silently stop being surfaced'
|
||||
fi
|
||||
|
||||
case "$OUT" in
|
||||
*"Rule 0"*) ok 'names rule 0' ;;
|
||||
*) no "does not name rule 0: '$OUT'" ;;
|
||||
esac
|
||||
|
||||
# Each of the four steps, individually. Naming rule 0 while having quietly lost
|
||||
# a step is the plausible drift, not losing the whole line.
|
||||
for step in brain_query skill 'failing test' 'success criterion'; do
|
||||
case "$OUT" in
|
||||
*"$step"*) ok "states the '$step' step" ;;
|
||||
*) no "lost the '$step' step from the ritual" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 4. The interpreter named in the registration must actually exist, or the hook
|
||||
# fails on that host with a message nobody reads.
|
||||
INTERP=${CMD%% *}
|
||||
if command -v "$INTERP" >/dev/null 2>&1; then
|
||||
ok "registered interpreter '$INTERP' is available"
|
||||
else
|
||||
no "registered interpreter '$INTERP' not found on this host"
|
||||
fi
|
||||
|
||||
printf ' %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -24,7 +24,7 @@ decision_for() {
|
||||
_tool=${2:-Bash}
|
||||
jq -n --arg t "$_tool" --arg c "$_cmd" \
|
||||
'{tool_name: $t, tool_input: {command: $c}}' \
|
||||
| sh "$GUARD" \
|
||||
| "$GUARD" \
|
||||
| jq -r '.hookSpecificOutput.permissionDecision // "MISSING"'
|
||||
}
|
||||
|
||||
@@ -42,6 +42,20 @@ expect() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Invoked as a PROGRAM above, never as `sh "$GUARD"`. Claude Code executes the
|
||||
# hook directly, so its `#!/usr/bin/env bash` shebang decides the interpreter;
|
||||
# piping it into `sh` overrides that and tests something production never runs.
|
||||
#
|
||||
# It is not academic. On Debian /bin/sh is dash, which has no `set -o
|
||||
# pipefail`, so the guard died on line 23 before reading a byte of input and
|
||||
# every deny case returned empty -- 20 of 20 assertions failed in CI while
|
||||
# passing on a host where /bin/sh happens to be bash. A security gate that
|
||||
# tests green and cannot start is the worst of both.
|
||||
if [ ! -x "$GUARD" ]; then
|
||||
printf 'FAIL: %s is not executable -- it must run via its own shebang\n' "$GUARD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$GUARD" ]; then
|
||||
printf 'FAIL: %s does not exist\n' "$GUARD"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user