Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8359d61e33 | ||
|
|
400d2b9b6b | ||
|
|
4edf42ce9f | ||
|
|
2f53346b8c | ||
|
|
dcc0677270 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills",
|
"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.",
|
"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": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
@@ -41,7 +41,17 @@ if [ ! -f "$CONFIG" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ENVFILE" ]; then
|
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 "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
|
exit 2
|
||||||
fi
|
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}
|
_tool=${2:-Bash}
|
||||||
jq -n --arg t "$_tool" --arg c "$_cmd" \
|
jq -n --arg t "$_tool" --arg c "$_cmd" \
|
||||||
'{tool_name: $t, tool_input: {command: $c}}' \
|
'{tool_name: $t, tool_input: {command: $c}}' \
|
||||||
| sh "$GUARD" \
|
| "$GUARD" \
|
||||||
| jq -r '.hookSpecificOutput.permissionDecision // "MISSING"'
|
| jq -r '.hookSpecificOutput.permissionDecision // "MISSING"'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,20 @@ expect() {
|
|||||||
fi
|
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
|
if [ ! -f "$GUARD" ]; then
|
||||||
printf 'FAIL: %s does not exist\n' "$GUARD"
|
printf 'FAIL: %s does not exist\n' "$GUARD"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user