The Semgrep plugin currently does not work correctly when used through
Claude because it is located within a subdirectory of the Semgrep
Marketplace repository. This issue was reported in:
https://github.com/anthropics/claude-plugins-official/issues/450
Previously, this could not be fixed due to a limitation in Claude Code's
handling of plugins located in subdirectories. Support for this was added
with the git-subdir feature, released in v2.1.69:
https://github.com/anthropics/claude-code/issues/30593
A fix for the Semgrep plugin was proposed once this version became the
latest release. Now that v2.1.69+ is available as latest, this PR
implements that fix.
https://claude.ai/code/cse_01RtW9KS12VZNFfWmWY6z9Pu
Co-authored-by: Claude <noreply@anthropic.com>
- Split pipeline into two steps (extract lines, then parse) mirroring the
original structure.
- set +e around the jq call so failures reach the $? check instead of
aborting under set -e.
- The "no text content" branch remains removed (that was the original bug —
all-tool-use turns now correctly yield empty text and the loop continues).
The state file lives at .claude/ralph-loop.local.md — project-scoped,
not session-scoped. The plugin's Stop hook fires in every Claude Code
session open in that project directory. So if session A starts a loop,
session B's Stop events also find the state file and block, feeding A's
prompt into B and consuming A's iteration budget.
This was masked by the transcript-parsing bug fixed in the previous
commit: that bug deleted the state file on the first Stop in any
session, so neither session looped. Fixing it exposed the leak.
Fix: setup writes CLAUDE_CODE_SESSION_ID into the frontmatter; the hook
compares against .session_id from its stdin JSON and exits silently on
mismatch. State files without session_id (written by old setup scripts)
fall through to preserve existing behavior.
Claude Code writes each assistant content block (text/tool_use/thinking)
as its own JSONL line. The hook's `grep role:assistant | tail -1` would
grab whichever block happened to be last — often tool_use — then jq's
text filter returned empty string, triggering the 'no text content' path
which deletes the state file and exits without blocking.
Net effect: the loop silently never fires. In one observed session, 62%
of assistant lines were tool_use-only; the hook deleted state on the
very first Stop event every time.
Fix: slurp all assistant lines with jq -rs, flatten to text blocks only,
take the last. Empty result is now non-fatal — no text means no <promise>
tag, so the loop continues. Also absorbs jq parse errors (control chars
in text) via || fallback instead of aborting under set -e.
The detectFileType function matched any .md file under an agents/ or
commands/ directory, including those nested inside skill content (e.g.
plugins/foo/skills/bar/agents/). These are reference docs, not plugin
agent definitions. Only validate agents/ and commands/ at the plugin
root level.