Skip to main content

tool-result-traps

Tool results that mean something other than they look — a pattern that never compiled, a glob that cannot match a directory, one tier searched but reported absent, a rejected flag reading as "no results". Use when an empty result is about to gate an action, land in a doc, or be reported as done.

ソース情報

リポジトリ
laurigates/claude-plugins
ソースの最終更新活動
2026年9月24日 20:02
検出された SKILL.md の言語
英語
スター
60
フォーク
6

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
2 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
tool-result-traps
description
Tool results that mean something other than they look — a pattern that never compiled, a glob that cannot match a directory, one tier searched but reported absent, a rejected flag reading as "no results". Use when an empty result is about to gate an action, land in a doc, or be reported as done.
allowed-tools
Read, Glob, Grep, Bash(rg *), Bash(git grep *), Bash(git log *), Bash(git status *), Bash(git worktree *), Bash(gh api *), Bash(gh pr view *), Bash(python3 *), TodoWrite
model
opus
created
2026-08-21T00:00:00.000Z
modified
2026-09-24T00:00:00.000Z
compatibility
claude-code
reviewed
2026-08-21T00:00:00.000Z
# Tool Result Traps Promoted from the always-loaded `~/.claude/rules/tool-use-patterns.md` rule, whose stub points here. The section bodies below are that rule's verbatim text. One law across all of them: **an empty result, a green exit, and a well-formed line of output are each claims about *mechanics*, not about *content*.** Every trap here produces output that is indistinguishable from a correct answer — no error, no warning, nothing on stderr — and the damage lands when that output is used as a verdict: "no duplicate exists", "the rename is complete", "nothing was lost", "the agent got its input". In one case the diagnostic *was* written — and the caller's own `2>/dev/null` is what made it disappear. The recurring fix is equally uniform: **control-test any negative that gates an action.** Re-run the same command shape against something you *know* is present. If the control also comes back empty, the tool is broken, not the tree clean. One control run is cheap; a wrong negative is a confidently-reported non-finding. ## When to Use This Skill | Use this skill when... | Skip when... | |---|---| | A zero-match or empty result is about to be reported as "clean", "complete", or "none found" | The result is non-empty and you are acting on what it contains | | Deduping before filing an issue, or concluding nobody reported something | Reading a single known file whose path you just listed | | Declaring a bulk rename, migration, or sweep finished | Mid-sweep, still transforming matches | | A verification loop's input set is built from a relative path | The path was resolved absolutely in the same command that used it | | Batching parallel tool calls whose siblings may exit non-zero | A single call, or a batch of confirmed-present paths | | A `Workflow` script's agents report thin or generic findings on rich material | Agents are returning specific, grounded detail | | An `rg`/`git grep` result contradicts something you read directly | Output matches an independent read of the same file | | Concluding a skill, command, recipe, or binary "does not exist" | You enumerated every tier it could be defined in | | A `-g`/`--glob` filter is doing the narrowing, and the name you want is a directory | The glob is `**/<name>/**`, or the search is on content not names | | The command that produced the empty result had its stderr redirected away | You read the command's stderr, or have seen it fail before | ## The traps ## Grep / rg — `-r` is `--replace`, not a bundled short flag `rg`'s `-r` takes an argument: it **rewrites every match in the output**. Bundling it into a short-flag cluster silently consumes the next letter as the replacement string, so the tool prints *fabricated* lines that look like real file contents. ``` # Wrong — reads as "recursive + line numbers"; actually means --replace=n rg -rn "yolo" . ./conf/cli_clients/gemini.json: "--n" ← the file says "--yolo"; rg rewrote it # Right rg -n "yolo" . ./conf/cli_clients/gemini.json: "--yolo" ``` The failure is **silent and confident**: no error, no warning, and the output is well-formed — it just doesn't match the file on disk. Observed 2026-07 building a false picture of a config file that was then nearly acted on; caught only because the doctored line contradicted an earlier direct `Read` of the same file. - **`rg` is recursive by default** — there is no `-r` to add. The instinct is imported from `grep -r`, and that's the trap. - **Never bundle `-r` into a cluster.** If an `rg` result contradicts something you read directly, suspect the flags before you suspect the file. - **Prefer the Grep tool** over `rg` in Bash: it has no `--replace` surface, so this class of error cannot occur. ## `git grep -E` has no `\b` — the pattern matches nothing, silently `git grep`'s `-E` is POSIX ERE, where `\b` is undefined. A word-boundary pattern therefore matches **nothing at all** — no error, nothing on stderr, just an empty result and exit 1. Exit 1 from grep *means* "no matches", which is precisely what a genuinely clean result looks like, so nothing anywhere signals that the pattern was never valid. ``` git grep -nE '\bprisma\b' -- package.json # rc=1, 0 lines, stderr empty git grep -nP '\bprisma\b' -- package.json # rc=0, 19 lines ← PCRE, works git grep -nwE 'prisma' -- package.json # rc=0, 19 lines ← -w, works grep -nE '\bprisma\b' package.json # rc=0, 19 lines ← GNU/BSD extension ``` The trap is that `\b` **does** work in plain `grep`/`rg`, so the habit is well-formed everywhere except the one tool that silently drops it. Worst case is a **completion verdict on a bulk sweep**: `git grep -E '\btrends\b'` returning empty reads as "the rename is complete" when it never searched for anything. Observed 2026-08 on a 112-file rename; caught only by the known-good control run that `never-fabricate-test-identifiers.md` requires. - **Use `-w` for word boundaries in `git grep`**, or `-P` for full PCRE. Reach for plain `grep -E` / `rg` outside a git-tracked scope. - **A zero-match sweep verdict must be control-tested** — re-run the same pattern shape against a term you know is present. If the control is also empty, the pattern is broken, not the tree clean. - Piping (`| wc -l`) masks the exit code entirely, so even the rc=1 tell is gone — see *A pipe discards the command's exit code* below for the general case. ## A wrapped string defeats a source grep — the code is unchanged, the grep says fixed A string literal split across source lines for line-length reasons exists nowhere contiguously in the file, so grepping the *rendered* message finds nothing — and that zero reads as "the text is gone, someone fixed it." > Observed 2026-08 (loractl). Checking whether an error still blamed f16 range > overflow unconditionally, `git grep -c 'exceeded f16'` returned nothing and was > recorded as "reworded — task closable." The message was fully intact; the > source wraps it as `"...an activation exceeded \` + `f16's range; try f32..."`, > so the phrase spans two lines. A second claim was mis-cleared the same way in > the same pass, and both were recovered only by the control test. - **Grep a fragment that cannot straddle a wrap** — one distinctive word, or the symbol that owns the message (`check_step_loss`), never the whole sentence. - **Then read the hit.** The search locates the text; the verdict comes from reading it. - The control test in the section above catches this class. Run it on any negative that closes a task or reports something already fixed. ## A pipe discards the command's exit code — `| tail` reports success for a failed run A shell pipeline exits with the status of its **last** command, so `<cmd> | tail`, `| head`, `| grep`, `| wc -l` all throw away the status of the thing you ran. The result is not merely lossy, it is confidently wrong: a failing build reports success. This is the general case of the `| wc -l` note in the grep section above. > Observed 2026-08 (loractl). `just test 2>&1 | tail -25` returned exit 0 and was > written up as "suite green" — the 0 was `tail`'s. The 25-line window also showed > only the trailing `cargo test --examples` invocation (four targets, 0 tests > each) while the real results had scrolled past, so both halves of the report > were wrong. It nearly gated a commit on an unverified suite. Re-run with a > redirect: 384 passed across 79 targets, status from `just` itself. - **Redirect, don't pipe**, whenever the status matters: `cmd > out.log 2>&1; echo "EXIT=$?"` — then read the file. - `set -o pipefail` fixes the status but **not** the truncation, and it does not apply to a command the harness runs on your behalf. - **A CI watch has the same shape**: `gh pr checks <n> --watch | tail` reports the watcher's status, not the checks'. Read the states back explicitly (`--json name,state`) before calling a PR green. ## A `-g '*name*'` glob cannot match a **directory** name Two composing rules decide what a glob matches, and neither is visible in the output: 1. **A glob containing no `/` is matched against the *basename* only** — at any depth. This is why `-g '*.md'` works recursively. 2. **A glob containing any `/` is anchored to the full path from the search root**, and `*` does not cross `/`. Only `**` spans depth. So a name you are hunting that is a **directory** — a skill dir, a package dir, a fixture dir — is unreachable by the glob everyone reaches for first: ``` tree: skills/sentry-triage/SKILL.md rg -uu --files -g '*.md' -> skills/sentry-triage/SKILL.md ← basename rg -uu --files -g '*sentry-triage*' -> (nothing) ← basename is SKILL.md rg -uu --files -g '*sentry-triage*/**' -> (nothing) ← has '/', now anchored at root rg -uu --files -g '*/SKILL.md' -> (nothing) ← '*' can't cross the 2nd '/' rg -uu --files -g '**/sentry-triage/**' -> skills/sentry-triage/SKILL.md ← works ``` **The failure is worse than an empty result.** `-g '*name*'` still matches *sibling files* whose basename contains the string, so a real tree returns `sentry-triage-notes.md` — one plausible hit. An empty result at least invites suspicion; a partial one reads as "the search ran fine, the file isn't there," and it is the sibling that sells it. > Observed 2026-08 (`repos-claude-config#32`): confirming whether a > `/sentry-triage` command existed. `rg --files -g '*sentry-triage*'` over > `~/repos ~/.claude` returned only a stray `.md`, which was written into a > published doc and a PR body as "no command by that name exists". It existed — > `ForumViriumHelsinki/infrastructure/.claude/skills/sentry-triage/SKILL.md`. > Caught only by a known-good control (`repo-activity`, a skill known to be on > disk) returning zero through the identical glob. - **Matching a directory name → `-g '**/<name>/**'`.** Nothing shorter works. - **Prefer `rg -l <pattern>` or `find -type d -name` when hunting a *name*** — a content search has no basename rule to trip over. - **Control-test with a name you know is present**, through the byte-identical glob. The control is what separates "not there" from "unmatchable". ## One search tier is not the search universe A Claude Code skill or command resolves from **three independent tiers**, and finding nothing in one says nothing whatsoever about the others: | Tier | Location | |---|---| | User-global | `~/.claude/skills/`, `~/.claude/commands/` | | Plugin | `~/.claude/plugins/cache/<marketplace>/<plugin>/skills/` | | **Project** | **`<repo>/.claude/skills/`, `<repo>/.claude/commands/`** | The project tier is the one that gets missed, because it is not under `~/.claude/` at all — it ships inside whatever repo happens to be the checkout, so the *same* command exists or doesn't depending on where a session is rooted. That is a live precondition for a scheduled task or a cloud routine: a project-scoped command resolves only when its repo is the clone. The same shape recurs wherever definitions are tiered — shell functions vs. `$PATH` binaries, `just -g` recipes vs. a local `justfile`, global vs. project MCP servers, user vs. repo git config. - **Enumerate all three before concluding a command does not exist.** In this portfolio, `rg -uu --files -g '**/<name>/**' ~/.claude ~/repos` covers the user and project tiers in one pass (note the glob form — see the trap above). - **A marketplace search is not a project search.** `gh api search/code` over the plugin repo answers the plugin tier only. - **State the tier you searched** when reporting a negative. "Not in `~/.claude/`" is a fact; "does not exist" is a claim about all three. ## A rejected flag looks exactly like "no results" Any `cmd … | jq/grep` whose **non-zero exit** yields empty stdout masquerades as a legitimate empty result. Worst case is a **dedup step**: you conclude nobody reported the bug and file a duplicate. Live instance: `gh search issues --state all` is invalid (that flag takes only `{open|closed}`; `all` belongs to `gh issue list`). It prints usage to stderr, so a `--jq` pipeline emits nothing — six consecutive false "no duplicate" verdicts. Use `gh api --paginate "repos/O/R/issues?state=all"` + `grep` instead (it returns PRs too; discriminate on `.pull_request`). **The same trap on a *write*, which is worse.** A rejected flag on a command meant to *change* something reports nothing and changes nothing, so "no output" reads as success. Observed 2026-08: `gh issue comment <n> --body … --jq .html_url` — `gh issue comment` has no `--jq` (it prints a URL, not JSON). The call emitted nothing and posted no comment, so the cross-link between two freshly-filed issues simply did not exist. Caught only by reading the issue back afterwards. On a read you get a wrong answer; on a write you get a **silently skipped action you will later report as done**. **Worse still: an *accepted* flag that takes your stdin marker literally.** The two cases above at least do nothing. A flag that is valid but means something other than what you assumed writes **wrong content, successfully** — exit 0, a URL printed, nothing to notice. Observed 2026-08: ``` # Wrong — --body takes a literal string, so the body becomes "-" gh pr create --title "…" --body - <<'EOF' ## What … EOF ``` `gh`'s `--body` is a plain `string`; only `--body-file` documents `"-"` as stdin (same split on `gh pr create`, `gh issue create`, `gh pr comment` — check with `gh <cmd> --help | grep -- --body`). The heredoc was piped to a stdin nobody read, `-` became the entire PR description, and the PR rendered as one empty bullet. Caught only when a human said the description looked wrong. - **Write the body to a file and pass `--body-file <path>`** (or `--body-file -` if you really want stdin). This also dodges the multi-line quoting mess — same instinct as `copy-paste-commands.md`. - Append `; echo "EXIT=$?"` to any one-shot mutating `gh`/`git` call whose output you are not otherwise reading. - **Verify the side effect, not the exit code**, for anything you will tell the user is complete — re-read the comment, the label, the pushed ref. For a body you authored, read it *back*: `gh pr view <n> --json body --jq '.body | length'` against a length you expect. A 1-char body is the tell. **Control-test every negative that gates an action.** Re-run the same command shape against a term you know is present; if the control also returns nothing, the tool is broken, not the result empty. One control run caught all six above. This is `never-fabricate-test-identifiers.md`'s known-good control, applied to search. ## Your own `2>/dev/null` turns a loud rejection into a clean negative The section above is about a tool that stayed quiet. This one is its mirror, and the difference is the whole point: **the tool did its job.** It rejected the command, printed a diagnostic, and exited non-zero — and the caller's own redirect threw all three away. No tool-side improvement reaches this: the message was written and then discarded downstream of the tool. ``` # Wrong — gh pr diff takes no pathspec, and the rejection goes to /dev/null gh pr diff 36 -- .env.example 2>/dev/null | grep -E "^[+-]" ← empty stdout, reads as "this PR does not touch that file" # The same command with the redirect dropped gh pr diff 36 -- .env.example accepts at most 1 arg(s), received 2 ← rc=1, and it said so all along ``` Observed 2026-08 reviewing a PR that claimed `Closes #29`: the empty result was one step from being reported as "the change was never made". The file was `+34/-5`. The exit code was lost as well — a pipeline reports the status of its **last** command, so `grep`'s status is what survived, not `gh`'s. **The control test does not catch this class.** Re-running the same shape against a file the PR definitely touches comes back empty too — the shape is broken for every input, so the control agrees with the false negative and confirms it. What breaks it open is dropping the redirect, not changing the input. - **Suppress stderr only on a command whose failure mode you have already seen.** `2>/dev/null` is a claim that you know what would have been printed. On a first-time shape — a new flag, a new subcommand, a line copied from elsewhere — it mutes the one channel that would say the command never ran. - **Re-run without the redirect before believing a negative that gates an action.** One run, and the diagnostic is either there or it is not. Prefer keeping stderr and reading it (`2>&1`) over muting it while you are still learning a command's shape. - **Correct forms for the case above**: `gh pr diff <N>` and filter the unified diff yourself, or `gh pr view <N> --json files` for per-file additions and deletions. For the worktree-shell wedge, the vacuous path-scoped verification, the `Workflow` `args` JSON-string trap, the control that must exercise the failing part of the pattern (the `parseFloat` case), and the parallel-batch / agent fan-out hazards, see [REFERENCE.md](REFERENCE.md).
GitHubで見る