The round-trip test added in this PR failed on ubuntu-latest while passing
on macOS: after Read → Stop, a protected block containing a backslash came
back altered (byte 109, line 3 of the fixture).
expand_file rebuilds each line with `${line//$placeholder/$block_content}`.
Bash 5.2 enables patsub_replacement by default, which makes the replacement
side of that expansion special: an unquoted `&` expands to the matched text
and backslashes are consumed. Block content is raw code and routinely
contains `&&` and backslashes, so on any bash 5.2 host every protected block
with either character was corrupted when it was expanded back. Bash 3.2 on
macOS has no such option, which is why local runs never showed it.
Turn the option off at the top of the hook; older bashes reject the shopt,
so the error is ignored there. Extend the byte-fidelity fixture with `&&`
and `&` so the ampersand case is guarded alongside the backslash one.
When a Stop leaves no BLOCK_ placeholder to expand, the hook has to restore
the backup to recover the protected block — which overwrote whatever had
replaced the file. Copy that content into the cache as <id>.recovered first
and name both paths in the warning, so the rewrite is merged back by hand
rather than lost.
Adds a test that the fallback keeps the rewritten content in the cache, and
a minimal lifecycle test (Read, a direct write with no event, Stop) asserting
the write survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Stop branch restores every cached file from its backup, but that
backup is written on PreToolUse Read and refreshed only by the
PostToolUse Edit|Write branch. A change made to a filtered file through
any other route between Read and Stop — a Bash command, a formatter, an
external editor — is overwritten at session end. Reproduced in #295:
Bash wrote const editable = 2 and read it back, Stop left const
editable = 1 on disk, with no crash and no failed tool call.
The hook protects the content of an annotated block, not the file: the
header documents the backup as holding the model's changes, and Edit and
Write already preserve them. So Stop now runs the same placeholder
expansion the Edit|Write branch runs, against the file as it is on disk.
Whatever wrote the file wrote its filtered form with the placeholders
intact, so expanding it restores the protected blocks and keeps the
change, whichever route it came from.
The expansion body moves into an expand_file function that both branches
call, so the fuzzy-match and deleted-block warnings apply at Stop too.
When no cached BLOCK_<hash> is left, the file was rewritten wholesale and
the hook cannot place the block; it falls back to the backup as before,
now with a warning instead of in silence. That residual is documented in
SIMPLIFY-IGNORE.md. With no outside write, expansion reproduces the
backup exactly, so Read→Stop and Read→Edit→Stop are byte-identical.
Test 11 drives the real hook through Read, an Edit event, a write with no
event after it, and Stop; it fails at be4e44a on the two assertions for
the lost write. Suite: 28 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin's SessionStart hook injected using-agent-skills/SKILL.md as
additionalContext on every session. Claude Code already discovers and
routes skills from their descriptions, so the hook stacks a second
router on the native one — the pattern docs/getting-started.md warns
against ("That creates two routers for the same task"), measured at
~3.5k wasted cache-creation tokens per session in #569.
Delete hooks/hooks.json (the registration; the plugin now ships no
hooks) and keep hooks/session-start.sh as a standalone script for hosts
without native skill routing, matching the fix shape suggested in the
issue. Update the hook header, CONTRIBUTING.md, codex-setup.md, and
gemini-cli-setup.md to match.
#474 changed session-start.sh to the host-required
hookSpecificOutput.hookEventName/additionalContext shape. The
regression test still expected the old {priority, message} payload, so
`bash hooks/session-start-test.sh` fails on current main.
Sabotage: the pre-change test errors with "expected IMPORTANT priority,
got undefined". After this change both the jq and jq-missing paths pass.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The session-start hook emitted {"priority", "message"} JSON on all three
of its output paths. Codex CLI validates SessionStart hook output and
fails the hook on that shape ('hook returned invalid session start JSON
output', #465); the documented contract for both Codex and Claude Code is
{"hookSpecificOutput": {"hookEventName": "SessionStart",
"additionalContext": "..."}}
Switch all three paths (meta-skill injection, jq-missing fallback,
meta-skill-missing fallback) to that envelope. Verified each path emits
valid JSON of the new shape: happy path via jq shape assertion, jq-missing
via PATH='' run, meta-skill-missing via a copy outside the repo.
Unquoted ${CLAUDE_PROJECT_DIR} in SDD-CACHE and SIMPLIFY-IGNORE setup
examples breaks when the project path contains spaces, the same class
of failure fixed for SessionStart in #214.
Co-authored-by: Cursor <cursoragent@cursor.com>
Test 10 pipes malformed JSON through the real hook, but the hook's jq
guard exits with 'error: missing jq' before parsing on machines without
jq (default Git for Windows, minimal Linux). The suite then reports a
failure unrelated to the contributor's change; tests 1-9 only exercise
the extracted filter_file and keep passing.
Mirror session-start-test.sh, which CONTRIBUTING documents as validating
both the jq and no-jq branches: with jq on PATH keep the existing
bad-JSON warning assertion, without it assert the documented missing-jq
guard message.
Verified on Windows (Git Bash): 21 passed, 0 failed both with jq 1.8.1
on PATH and without jq.
hooks/simplify-ignore-test.sh was committed with mode 100644 while its
sibling test script hooks/session-start-test.sh is 100755. Both share
the same #!/bin/bash shebang and live in the same directory; set the
exec bit for consistency.
When users manually copy hooks to .claude/ instead of installing via plugin, ${CLAUDE_PLUGIN_ROOT} is not set and the SessionStart hook silently fails.
This adds a fallback to ${CLAUDE_PROJECT_DIR}/.claude/hooks/ so the hook works in both plugin and manual install modes. If neither path exists, it silently succeeds.
The fix in 43a0dde introduces jq as a hard runtime dependency for the
session-start hook. jq is not preinstalled on every macOS or Linux setup,
so users without it would hit a cryptic failure on every new session.
Add a `command -v jq` guard that emits a valid INFO-priority JSON message
explaining the dependency and pointing at install commands. Skills remain
available individually — only the meta-skill injection is skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes issue #89: session-start hook now properly escapes SKILL.md content
when constructing JSON output, preventing parse errors when markdown
contains quotes or control characters.
Uses jq -cn with --arg flag to properly escape all special characters
in the message field.
Drop the 24h TTL: HTTP validators are the whole freshness contract.
Key cache on URL alone; prompt-aware keying with normalization gave
false safety (semantic differences slipped through). Prompt is kept
as metadata and surfaced in the hit message so the next agent can
judge whether the earlier reading applies. Reframe docs around
"HTTP resource cache, not prompt cache".
While here, fix two latent bugs:
- Replace the unquoted heredoc in the pre-hook with printf. The
heredoc expanded $vars and backticks inside cached content, so a
compromised doc page could trigger command substitution on cache
hit.
- Strip CR before awk paragraph-mode parsing of curl -I -L output
so blank separators between response blocks on a redirect chain
are recognised (was silently picking intermediate headers).
Remove dead -v IGNORECASE=1 (gawk-only; tolower() already handles it).
Adds a pair of optional Claude Code hooks that cache WebFetch output
on disk but revalidate every reuse against the origin. Content is
served only when the server returns 304 Not Modified, so
source-driven-development's "verify against current docs" guarantee
still holds across sessions.
- hooks/sdd-cache-pre.sh: PreToolUse hook. For a cached entry, issues
a HEAD with If-None-Match / If-Modified-Since. On 304, blocks the
WebFetch (exit 2) and returns cached content via stderr; otherwise
allows the fetch through.
- hooks/sdd-cache-post.sh: PostToolUse hook. Captures response plus
current ETag / Last-Modified. Entries without a validator are
never stored — without one, the pre hook cannot verify freshness
and caching would amount to trusting memory.
- Cache key: sha256(url + normalized_prompt). Prompt is lowercased
and whitespace-collapsed so stylistic variants hit the same entry;
semantically different prompts still miss.
- Hard 24h TTL as a safety net against misbehaving origins.
- hooks/SDD-CACHE.md: opt-in setup, end-to-end testing, debugging.
- .gitignore: ignore the .claude/sdd-cache/ directory.
Hooks are opt-in: users register them in .claude/settings.json. The
source-driven-development skill itself is unchanged.
Enable this repo to work as a Claude Code plugin so users can
install it via `/plugin marketplace add addyosmani/agent-skills`.
Add:
- .claude-plugin/plugin.json — plugin manifest exposing
.claude/commands/ as namespaced slash commands under
the agent-skills: prefix
- .claude-plugin/marketplace.json — marketplace catalog
listing this repo as the agent-skills plugin
Fix:
- hooks/hooks.json — rewrite from deprecated flat-array format
to the plugin-system format keyed by event type; use
${CLAUDE_PLUGIN_ROOT} so the hook resolves correctly when the
plugin is installed from the marketplace into the local cache
- README — replace non-existent `claude plugin add` with the
correct `/plugin marketplace add` workflow; document namespaced
slash commands and --plugin-dir usage; add .claude-plugin/ to
the project tree
The repository was renamed from swe-skills to agent-skills but
internal references were never updated. This caused:
- session-start.sh to silently fail (path using-swe-skills/ did
not exist; the directory is using-agent-skills/)
- slash commands to invoke non-existent swe-skills: namespace
- docs and setup guides to point at the old clone URL
Rename all occurrences across 14 files: commands, CLAUDE.md,
README tree, docs/, and hooks/.