Skip to content

fix(plugin-dev): validator scripts abort on first finding due to set -e - #66416

Open
wellkilo wants to merge 1 commit into
anthropics:mainfrom
wellkilo:fix/plugin-dev-validator-set-e
Open

wellkilo wants to merge 1 commit into
anthropics:mainfrom
wellkilo:fix/plugin-dev-validator-set-e

Conversation

@wellkilo

@wellkilo wellkilo commented Jun 9, 2026

Copy link
Copy Markdown

Problem

Three validator scripts in plugin-dev use set -euo pipefail:

  • plugins/plugin-dev/skills/agent-development/scripts/validate-agent.sh
  • plugins/plugin-dev/skills/hook-development/scripts/hook-linter.sh
  • plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh

These are "check each item → accumulate errors/warnings → print a summary"
validators. But several commands they rely on legitimately return non-zero
during normal operation, and with -e any of them aborts the whole script
at the first finding — before the summary or correct exit code is reached:

  • ((counter++)) returns exit code 1 when the counter goes from 0 → 1
    (post-increment evaluates to the old value 0, which is arithmetically false).
  • grep '^description:' returns non-zero when an optional field is absent.
  • jq returns non-zero when indexing a malformed structure.

Result: the validators silently fail exactly when the input has problems —
the case they exist to handle.

Repro

$ bash validate-agent.sh some-agent-with-errors.md
# stops at the first ❌, no summary, output truncated

Fix

Change set -euo pipefail → set -uo pipefail in all three scripts.
-u and pipefail are kept. All fatal conditions (missing file, missing
args, malformed frontmatter) already use explicit exit 1, so error handling
behavior is unchanged.

Verification

  • Malformed inputs now run all checks and print the correct summary + exit code.
  • Valid inputs still exit 0 with "✅ All checks passed!".
  • bash -n passes on all three files.

Change-Id: Ib82726bd6d85b8a98b6e3210a31944288a9eab3c

@stevei101 stevei101 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved by Antigravity AI pair programmer after verifying CI checks pass.

@wellkilo

Copy link
Copy Markdown
Author

@ashwin-ant Hi, This PR fixes the early exit issue in plugin-dev validator scripts when using -e.
The changes have been reviewed and approved by @stevei101, but I still need a maintainer's approval for the workflow and merge to proceed.
Could someone please take a look when you get a chance?
Thanks a lot!

@konsta95

konsta95 commented Sep 3, 2026

Copy link
Copy Markdown

Independently verified this on main (f173a69), bash 5.3, jq 1.8.1. I applied the three-line change to fresh copies of the three scripts — diff against the base reproduces this PR's hunks exactly — and ran both versions over a 17-case fixture set. Summary: the fix works, and it also uncovers a second bug that set -e was hiding.

The fix works

Format is exit / error-lines / warning-lines / final-summary-printed.

input before after
validate-hook-schema.sh — one low timeout 1 / 0 / 1 / no 0 / 0 / 2 / yes
validate-hook-schema.sh — 2 hooks, 1st missing matcher 1 / 1 / 0 / no 1 / 2 / 0 / yes
validate-hook-schema.sh — 6 seeded defects 1 / 1 / 0 / no 1 / 6 / 6 / yes
hook-linter.sh — script with no shebang 1 / 1 / 3 / no 1 / 2 / 3 / yes
validate-agent.sh — 5 seeded defects 1 / 0 / 1 / no 0 / 0 / 5 / yes
(control) valid config / valid agent / clean script unchanged unchanged

The three controls are the point: base and patched are byte-identical on every input with no -e trigger, and differ on all 11 that have one.

Two cases are worth spelling out because they're worse than "stops early":

hook-linter.sh silently skips files. Linting three scripts where the second has an error:

$ bash hook-linter.sh warn-only.sh err1.sh err2.sh
...
❌ Found 1 error(s) and 3 warning(s)     <- err1.sh
$ echo $?
1

err2.sh is never opened, and N script(s) had errors never prints. Both versions exit 1, so a caller cannot distinguish "linted everything, found errors" from "gave up at the first one". Note this file needs an error-producing target to reproduce — its 11 in-function increments are harmless, since check_script is called as if ! check_script "$script", which suppresses set -e inside the function. The live one is the single top-level ((total_errors++)) on line 142. I first tested with a warnings-only fixture and wrongly concluded this file was unaffected.

validate-agent.sh fails valid files with no message. An agent that merely omits the optional tools: key prints four green checks and then vanishes — no error, no warning, no summary, exit 1 — because that optional-field extraction propagates a non-zero status. After the change: ✅ All checks passed!, exit 0.

What the fix uncovers: the schema validator can't read the plugin format

This isn't a regression — set -e was hiding it. validate-hook-schema.sh expects a bare event map, but a plugin's hooks.json wraps events under a hooks key alongside a description string. On main the run dies at the first jq failure with exit 5 and no verdict. With this PR applied it runs to completion and reports a large number of false errors.

All five hooks.json files shipped in this repo, via the command hook-development/SKILL.md step 6 tells authors to run:

plugin on main with this PR
explanatory-output-style exit 5, no verdict exit 1, 66 false errors
hookify exit 5, no verdict exit 1, 65 false errors
learning-output-style exit 5, no verdict exit 1, 63 false errors
ralph-wiggum exit 5, no verdict exit 1, 57 false errors
security-guidance exit 5, no verdict exit 1, 97 false errors

Five of five — that's every hooks.json in the repo, not a sample.

The mechanism is exact. The validator iterates jq -r 'keys[]', so $event takes the values description and hooks. Then hook_count=$(jq -r ".\"$event\" | length" ...) — and jq's length on a string is its character count. So for $event = description the loop for ((i=0; i<hook_count; i++)) runs once per character of the description, each pass emitting ❌ description[i]: Missing 'matcher' field. That gives:

false_errors == (.description | length) + (.hooks | length)

I tested that as a prediction rather than a description. Manipulating description length across 0/1/5/20/65/200 with events held at 1, and event count across 1–5 with description held at 10, plus the type cases jq treats specially — a description of null (length 0), of [1..7] (length 7), absent entirely, and the number 300 (numeric length is absolute value, so it predicts 301 errors). Twenty observations including the five real manifests: 20/20 exact, slope 1.000, intercept 0.000, zero residual, over a range of 1 to 301 errors. On unpatched main the same twenty inputs are flat at exit 5 with zero errors, so the dose-response appears only once -e is gone.

The error count of an Anthropic plugin is literally the length of its description string.

Why these are false rather than a real finding: the same SKILL.md prescribes this shape at line ~79 — "hooks field is required wrapper containing actual hook events… This is the plugin-specific format" — and then at line 707 tells the author to check it with a script that can't parse it. Either all five shipped plugins are malformed or the validator is, and the five match the documented shape.

Where that leaves the PR

set -e was converting a wrong answer into a crash. Removing it is right — exit 1 is the script's own documented failure code, and the output is now legible enough to diagnose instead of an unexplained exit 5. But the PR body's reasoning that "all fatal paths already use explicit exit 1" doesn't hold on this input: the jq failure is fatal to the run's meaning and nothing catches it. So this change is necessary but not sufficient — validate-hook-schema.sh also needs a root-structure guard that detects the wrapper form (and either descends into .hooks or reports one clear error), otherwise merging this turns a silent abort into 57–97 wrong errors on every official plugin.

Two other pre-existing gaps this change doesn't touch, for completeness: a hooks.json whose root is [], {}, or a bare scalar like 5 still reports ✅ All checks passed! and exits 0. The scalar case prints two raw jq errors and still claims success — the root iteration sits in a for word list, where the substitution's failure was never examined even with -e on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants