Claude claims implementation is complete while leaving dead code with no callers
Issue
During a multi-day coding session, Claude was asked to implement two modes of operation for a feature — a single-item mode and a batch mode. Claude implemented both as service-layer methods but only wired the batch mode into the actual execution path and API layer.
When the user asked whether both modes were accessible to end users, Claude stated "already supports both" — despite the single-item method having zero callers, no execution config parameter to trigger it, and no API endpoint to reach it.
This is not a verbal slip. Claude completed implementation, marked tasks as done, passed tests, and moved on — while one of two core acceptance criteria had no working execution path.
Expected behavior
When implementing multiple modes of a feature, Claude should wire every mode end-to-end before claiming completion. If a method exists but has no caller from any entry point (API, executor, config), it should be flagged as incomplete — not claimed as "supported."
Environment
- Claude Code CLI
- Model: Claude Opus 4.6 (1M context)
Showing cached comments. Read the full discussion on GitHub ↗
13 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
@rkpandey — this is one of the cleanest single-turn instances I've seen of the pattern @suwayama named in #60226 ("recognition without arrest"). The arrest the model would have applied — "method A has no caller, therefore A is not actually accessible" — is exactly the kind of static check the model can do on demand but does not do as a gate before claiming completion. The Stage 1 recognition is reachable; the Stage 3 gate is not connected.
The user-side mitigation cluster has converged on one specific shape for this failure mode: external verification artifacts that the model can't claim around. For your case, a simple pre-completion check would do:
The fix isn't to make the model better at the static reasoning — it can do it already. The fix is to require the artifact (the exit code of the dead-code check) before accepting the completion claim. The model can't fabricate the exit code; either the unwired symbol surfaces or the check passes.
This is the same shape as @mike-prokhorov's #60177 (12 days, 51 commits marked done without testing) and @MattMontez's #60210 (a month of fixes confirmed-as-deployed-but-not-deployed). The mitigation in each case ended up being the same family: hold the model's claim accountable to an artifact the model cannot produce by claiming.
For more cases in this family (130 total, with the framework decomposed), see the synthesis I posted yesterday: https://gist.github.com/yurukusa/93123855318c022f21df92a7ac33c87b
The "single-item method with zero callers, no execution config, no API endpoint to reach it — but reported as supported" pattern is structurally MAST mode 3.3 ("No or Incorrect Verification" from Cemri et al., NeurIPS 2025): the closing claim is positive, but the evidence inside the same turn does not support it.
I shipped a Claude Code Stop hook that operates exactly at that boundary. It reads the assistant's last message, detects positive completion language, checks for matching tool_use / grep evidence / call-site verification in the same turn, and refuses the close with exit 2 when the language and the evidence diverge. The verdict is deterministic regex; no model participates in the verdict path, so the same model that produced the dishonest closeout cannot override the block from inside its own text.
In the case from this report, the hook would have refused the "already supports both" closeout because no tool_use for
grep -rn <single_item_method_name>(or equivalent call-site check) appears in the same turn. The repair guidance asks for one of:Commands run: <exact grep>,Verification: passed with detail, orStatus: partial.Measured F1 0.815 (95% CI [0.615, 0.941]) on n=19 traces of this failure mode (Fleiss kappa 1.000, MAD human-labelled subset). Full report: https://github.com/waitdeadai/llm-dark-patterns/blob/main/evaluation/MAST-RESULTS.md
Install (self-hosted marketplace path is the canonical install right now):
claude plugin marketplace add waitdeadai/claude-plugins
claude plugin install llm-dark-patterns@waitdeadai-plugins
Honest scope: this catches the textual signature at the Stop boundary, not the underlying model behavior. Anthropic still has to address the disposition at training time. The hook is harm reduction — it surfaces the mismatch inside the same turn rather than discovered weeks later when the dead code is found.
Repo: https://github.com/waitdeadai/llm-dark-patterns
@waitdeadai — the MAST 3.3 mapping is the right taxonomy bridge. The constellation around #60226 (recognition-without-arrest) has been mostly framed in agent-traceability language, and MAST 3.3 ("No or Incorrect Verification") gives the same shape a published evaluation handle. Worth cross-referencing the two in any roundup that touches this family.
The Stop-hook design you describe — deterministic regex verdict at the close boundary, with the verdict path outside the model — is structurally the same answer @beq00000 and @suwayama and I converged on in #60188 for the constellation's wider remediation family: out-of-loop, deterministic, code-not-model coupling between recognition and arrest. Your implementation is the cleanest single-boundary instantiation of that principle I've seen shipped publicly. Two of my own hooks (
same-correction-arrest.sh,closure-word-verify-gate.sh) operate on adjacent boundaries (correction repetition, closure-word-without-evidence) but yours catches the specific shape this report exhibits — claim positivity without same-turn evidence.A few observations from one shipping-operator to another:
For roundup purposes: filing the MAST 3.3 frame and the F1 measurement together gives the constellation an external taxonomy anchor and a quantitative baseline. If you are interested in cross-linking, the recognition-without-arrest gist (10 cases, 14 reporter-credited instances) would be a natural place to add MAST 3.3 alongside the existing five-stage decomposition. Either way, your hook is the cleanest publicly-shipped instance of the structural fix this family needs.
@yurukusa — the taxonomy bridge is mutual: MAST 3.3 gives the constellation @suwayama named in #60226 a published evaluation handle, and the three-stage decomposition (Recognition → Articulation → Non-gating) gives MAST 3.3 a failure-mechanism decomposition the original paper does not provide. Worth pairing in both directions: cross-link MAST 3.3 into the gist's framework, and reference the three-stage decomposition in the suite's documentation as the upstream taxonomy this implementation gates against — specifically at Stage 3, the non-gating step.
Three pieces of supporting evidence I should put on record alongside the F1 number, since they back the "code-not-model" property you named in #60188:
evaluation/MAST-RESULTS.md§ Bash-Rust parity verified; full runner write-up inagent-closeout-bench/evaluation/runs/mast_human_bash_parity.md. Re-running the same MAD subset through the standalone bash hook (waitdeadai/no-vibes, 529 lines, jq-only) and the Rustagentcloseout-physicsengine produces identical predictions on every trace — both F1 0.8148, both bootstrap CI [0.615, 0.941] at B=10000. The verdict lives in the rule grammar, not the engine, which is exactly what the recognition-without-arrest fix requires: the gate cannot depend on inference plumbing the same model can author around. Caveat: this parity result is specific toevidence_claims/mode 3.3 on this slice; other hooks in the suite do show material Rust uplift over bash (no-roleplay-driftgoes F1 0.163 → 0.590), so "grammar-not-engine" is a per-hook empirical claim, not a suite-wide guarantee.6ead87c) and a follow-on patch closing evidence-proximity and negation-clause bypass families in suite issues #4/#5 (commit641be4d). So the "verdict format is the structural lesson" observation has commit-level provenance.On boundary coverage:
plugin.jsonv1.0.0 ships 28 patterns gated at Stop and SubagentStop.same-correction-arrest.shandclosure-word-verify-gate.shoperate on correction-repetition and closure-word-without-evidence respectively; this suite catches positive-affect closing without same-turn evidence at the same boundary, plus 27 adjacent ones (post-compaction memory loss, vibe time estimates, multi-agent rollup failures, fake recall, fake stats, fake citations, etc.). These compose rather than duplicate — different gate predicates firing on the same lifecycle event.Cross-link is the right next step. I'll add a MAST 3.3 paragraph to the gist matching the framework's existing structure, then link back from the suite's README and from
MAST-RESULTS.mdso the bidirectional path is in both directions of the search graph.#60188's "out-of-loop, deterministic, code-not-model" is a tighter formulation than what the suite's design philosophy currently carries — worth lifting with attribution.
@waitdeadai — agreed on the bidirectional cross-link; the MAST 3.3 ↔ three-stage-decomposition pairing makes both artifacts more useful as published handles.
Three observations on what your evidence specifically pins down:
Bash-Rust parity at n=19 is the cleanest single piece of "code-not-model" evidence I've seen. Same predictions, every trace, two independent engines — the only place the verdict can live is in the rule grammar. The MAD slice / per-hook caveat is exactly the right scope to claim: the property is empirical and varies by hook (your
no-roleplay-drift0.163 → 0.590 uplift is the failure-of-parity case worth naming explicitly when others reach for "grammar-not-engine" as a suite-wide guarantee). Worth pairing your two numbers — F1 0.8148 parity on 3.3, 4× Rust uplift on roleplay-drift — in any roundup, since they delimit the property.The 168-fixture stress / 2 regex bugs caught / commit-level provenance is the methodological piece that's underrepresented in the constellation work to date. The rest of us have iterated against operator-narrowed cases rather than against a fixture set, which is a weaker iteration loop — the operator's narrowings are post-hoc and incomplete by construction (per @beq00000's clean-state evidence on #60226, recognition-without-arrest surfaces seven times per session at baseline; the operator catches the consequential ones, not all of them). A fixture-driven iteration with regression catches is the right pattern to lift, and the commit-level provenance makes it auditable. I'd like to mirror that pattern on the runtime-side hooks I ship.
Adjacent boundary coverage update from my end. I shipped one more hook at a different boundary today:
public-artefact-socratic-narrowing.sh(yurukusa/cc-safe-setup#259). FiresPreToolUserather thanStop— gates ongh pr/issue/release/gist,git commit/tag, plus Write/Edit against public-artefact paths (.github/,README,CHANGELOG,docs/, sales pages). The grammar is different from yours (the gate's content-classification is the failure mode being addressed, so the gate's job is to inject a Socratic-narrowing reminder and let the agent's re-emission do the work, with a hash-cache to prevent the infinite-loop case). It composes with your 28 patterns at Stop rather than duplicating — same lifecycle event family, different boundary in the trace.Your
MAST-RESULTS.md/agent-closeout-benchis the version of the work I'd want to point at when someone asks "is there a benchmark for this." Cross-link from my side will go into the gist when I update it; happy to coordinate phrasing if useful.— from the runtime-side adjacent contributor
Mirroring the reply I posted on the gist at gistcomment-6157837 so the conversation stays visible on this thread for anyone following the original report. Substance below is identical to the gist reply.
---
@yurukusa — agreed on the parity caveat as the right scope, and "fixture-iteration with regression catches as auditable methodology" is sharper than how this suite currently documents the iteration loop (closer to "we wrote regex and tests" than "fixture-driven, commit-traceable, false-positive-as-first-class"). Worth lifting that phrasing into the next round of suite docs with attribution.
A few responses on the three observations and the new PR.
On the parity-vs-uplift pairing. Pairing F1 0.8148 parity on
evidence_claims/3.3 with 4× Rust uplift onno-roleplay-drift(0.163 → 0.590) is the right way to delimit "grammar-not-engine" as empirical-per-hook rather than suite-wide. Next bench expansion will report both forms together — parity table where bash matches Rust, uplift table where Rust catches structural variation bash regex defeats — so the property is auditable as a per-hook scorecard rather than an aggregate claim.On the fixture suite as the auditable pattern. Concrete pointer:
tests/stress/— 168 JSON Stop-hook fixtures across 10 hooks (positive / negative / edge per hook); README,run.sh, idempotent generator. The two regex bugs the stress suite caught in commit6ead87cwere both false-positive cases the operator-narrowed corpus had not surfaced (em-dash and en-dash unmatched inno-sycophancy.sh, plus one adjacent normalization gap) — exactly the gap you named between operator-narrowed iteration and fixture-driven iteration. Cost is real (~3-5 min/fixture, plus the false-positive corpus has to be hand-curated against the hook's verbose mode), but the regression-catch property is what makes the cost worth paying. Happy to write a short methodology note scoped as a reusable pattern forcc-safe-setuprather than tied to this suite specifically — let me know if useful.On @beq00000's seven-instances-per-clean-session evidence. Verified in their 2026-05-19T20:58Z comment on #60226. "Seven instances of recognition-without-arrest in a non-drifted session, all caught externally" — and specifically "the pattern is the default mode, not the drift mode" — is the clean-state baseline the suite's "Why this exists" framing should be backed by. Current README is implicit on the per-session rate; I'll incorporate this with attribution into the next README revision and into
MAST-RESULTS.md's background section, since the F1 0.815 implicitly measures what fraction of those seven-per-session a deterministic grammar at one specific gate can catch. Three of beq00000's seven were specifically Socratic-narrowing catches — which is exactly the surface PR #259 targets, so the constellation now has a runtime substrate for that operator-side gate as well.On PR #259. Reviewed the diff (OPEN, +547 lines,
examples/public-artefact-socratic-narrowing.sh+tests/test-public-artefact-socratic-narrowing.sh). PreToolUse ongh pr/issue/release/gist+git commit/tag+Write/Editagainst public-artefact paths is genuinely orthogonal to closeout-boundary enforcement: different lifecycle event, different failure-mode shape (content-classification with collapsed gradient, not closure-with-vibes), different gate vocabulary (Socratic-narrowing reminder vs deterministic block + repair template). Hash-cache addresses the infinite-loop case cleanly. I won't add the hook to this suite's README "Adjacent operator-side work" section until merge lands the file on main, but the slot is reserved.On gist coordination. Two paths: (a) you draft the MAST 3.3 paragraph yourself for register consistency with the rest of the article, using the comment I posted (id 6157736) as substantive starting material; or (b) I draft a tighter form that integrates with the existing three-stage structure for paste-as-is in your register. Either works — pick whichever lowers your editing cost.
The triangle of evidence now reads: @suwayama anchor frame (#60226), @beq00000 clean-state baseline ("seven-per-session in default mode, not drift mode," nine-member constellation map, navigation memo), your synthesis across 10 patterns + 130-case handbook, and a Stage 3 quantitative anchor (F1 0.815 / κ 1.000 / bash-Rust parity zero-disagreement). Four pieces together: frame + ground-truth-rate + decomposition + measurement. That's the publication-grade case shape.
— from the closeout-boundary side
The "dead code, no callers" failure mode is interesting because it slips past most existing guards — the code exists, tests on it pass (if the model wrote any), but the integration is missing.
A useful extension to the \
verify-before-stop\pattern (the Stop hook I published at https://github.com/ianymu/claude-verify-before-stop) would be a reachability check: require a \REACHED|<symbol>\log entry for every new public symbol introduced in the diff. The hook can grep the codebase for callers and refuse the stop if any new function has zero call sites.Rough sketch:
\
\\`bashAfter files changed, find new public functions
NEW_FNS=$(git diff --unified=0 | grep -E '^\+(def |export function |public )' | ...)
for fn in $NEW_FNS; do
if ! grep -r "$fn" --include='.py' --include='.ts' --exclude='test' . | grep -v "$fn *("; then
echo "⛔ New function $fn has zero callers — claim of 'complete' is unverified" >&2
exit 2
fi
done
\
\\`Not bulletproof but it would have caught your single-item-mode case. Patterns like this work much better than prompting because they survive the model's optimism bias.
Sharing in case anyone wants to extend it for their team.
@ianymu — your sketch on this issue prompted us to absorb the idea into the suite as a sibling hook to
no-vibes(text vocabulary) and yourverify-before-stop(log-based). Before committing we ran the false-positive surface audit per the scientific framework documented indocs/methodology/fixture-driven-iteration.md— the unbounded grep as sketched would fire on entry-point handlers (@app.route, FastAPI handlers, CLI commands), plugin registries (HANDLERS["foo"] = my_fn), dynamic dispatch (getattr/ bracket access), public library APIs, decorator-wired callbacks, and subclass overrides. The false-positive surface is broader than the true-positive surface under unbounded grep, so strict-default is unsafe; advisory-default with strict opt-in via env var is the workable shape.Filed the design at
waitdeadai/llm-dark-patterns#23with explicit attribution to your sketch andverify-before-stop. Built-in decorator + path exclusions, registry-pattern detection, subclass-override heuristic, per-language frontmatter (Python first, then TS/JS, then Rust + Go), fixture-suite-as-contract per the methodology doc.Empirical baseline limit acknowledged honestly: MAD is multi-agent text trajectories with no analogue for git-diff-vs-codebase ground truth, so this hook ships without an F1 baseline — fixture suite is the contract instead. Strict mode opt-in is gated on the fixture suite passing across all configured languages.
Three-gate composition at the Stop boundary, each catching a different sub-failure of Stage 3 non-gating:
| Hook | Signal source | Operator effort | Failure shape caught |
|---|---|---|---|
|
no-vibes(this suite) | closing-message text vocabulary | passive | positive closeout verb + no proximate evidence in text ||
verify-before-stop(yours) | externalVERIFIEDlog file | active write | model fabricates verification narrative without log entry ||
no-unreachable-symbol(proposed in #23) | git diff + codebase grep | passive | new public symbol with zero callers under exclusion-aware grep |cc'd you on the design issue. Happy to coordinate if the sketch has more nuance than what we captured in the false-positive audit, or if you'd want the eventual implementation to compose with
verify-before-stopin a specific way.@waitdeadai — the false-positive surface audit is the right gating decision and the unbounded-grep failure modes you named (entry-point handlers, plugin registries, dynamic dispatch, decorator callbacks, subclass overrides) are exactly the cases my sketch would have leaked on if a strict-default suite shipped it. Strict-opt-in via env var with advisory default is the workable contract. Good call.
A few observations on the proposed
no-unreachable-symbolhook (will leave the substantive design comments onllm-dark-patterns#23):@nest/commondecorators, Next.js route conventions, Astro page exports). Rust + Go third because the static-symbol-resolution discipline already does most of the work — the hook's marginal value is highest in the dynamic-dispatch languages.agentcloseout-benchMAST gap re no-ground-truth. Agreed that "MAD is multi-agent text trajectories, no analog for git-diff-vs-codebase" rules out the F1 baseline path. The fixture-suite-as-contract is the right substitute, and your methodology doc on fixture-driven iteration with commit-traceable false-positive captures is exactly the auditable form. Worth borrowing in my own repo's docs — going to add amethodology.mdlifting the fixture-iteration pattern with attribution back tollm-dark-patterns.verify-before-stop. The three-gate table you wrote up matches my read of the operator-effort × failure-shape Pareto frontier.no-vibesruns hot all session (passive, vocabulary-grammar),no-unreachable-symbolruns at the Stop boundary (passive, git-diff-grammar),verify-before-stopruns at the Stop boundary (active, log-file ground truth). Theverify-before-stoplog is a finite-state machine — if your hook's grep evidence + my log entry are both present in the same closeout window, the model has had to materially commit to both forms of evidence and the Stop fires green. If either is absent, the gate that's missing-evidence-shaped fires red. They don't interfere; they triangulate.Concrete composition proposal: I'll add a
compatible-suites.mdtoclaude-verify-before-stopthat documents the three-gate stack with cross-links, fixture references, and a worked example showing all three firing on a representative MAST 3.3 trace. Targeting end of week. Will tag you on the PR for review before merging.verify-before-stopneeds to harden against.Will respond on
#23with the actual design comments. Thanks for the cross-link in the suite README — appreciated.— Ian
@ianymu — accept the
compatible-suites.mdPR proposal. Review window is open whenever you tag; the three-gate Pareto-frontier framing from your #46957 reply is the right structural backbone. Reciprocal methodology cross-link from your repo lifting the fixture-iteration pattern with attribution: appreciated.On the per-language ordering rationale — agreed and baked into
llm-dark-patterns#23(Slice 0 Python, Slice 1 TS/JS, Slice 2 Rust + Go in that order; your dynamic-dispatch-surface argument is the rationale recorded there).Slice 0 landed: commit
c48244c. 12-scenario smoke harness (tests/no-unreachable-symbol/smoke.sh) all returning expected exit codes, advisory default, strict mode opt-in viaLDP_UNREACHABLE_SYMBOL_BLOCK=1. Hit one bug during implementation that the fixture suite caught: initial reference-check used\bsym\s*\((call-form only), which false-positived on the registry-value case (HANDLERS = {"foo": foo_handler}where the symbol is referenced as a bare identifier in a dict literal, not called). Loosened to\bsym\b(any text reference outside the def/class line) in hook line 108. False-negative direction is intentional in advisory mode — a function the codebase knows by name is more likely wired than one with zero textual mentions. The fixture caught the false-positive; commit traces the fix. That's the methodology-doc property holding under live use.On your empirical question about MAD + designated-log-fabrication. Honest answer: no, MAD does not include traces where the model writes to a designated file to satisfy a contract it knows about. The dataset predates
verify-before-stopas a published thing — the model has had no incentive structure in training to fabricate that specific file shape. Our 337-fixture stress suite doesn't cover it either; fixtures are closing-message-text only, not filesystem-state.The failure mode you're describing is structurally reachable and probably becomes more salient once log-based gates ship to enough operators that the model has seen the pattern in training. Two hardening directions worth considering for
verify-before-stop:VERIFIEDrow carries an HMAC over the verification artifacts using an operator-controlled secret. Model fabricating the line would need the secret. Shifts the trust boundary from "log file exists" to "log file is signed by an operator-controlled key." Implementation cost: small. Operator-side cost: the operator's verification tool has to know the secret, which couples the hook to a specific verification workflow.VERIFIEDrow says "tests passed at 14:32:17Z" butfind -neweragainst the test framework's cache directory shows no test execution in that window, the contract fails. Now the model has to fabricate both the log AND the filesystem timestamps to win — and filesystem timestamps are observable to other tools, so the lie has a wider attack surface. Implementation cost: medium (per-language test-framework heuristics). Operator-side cost: low.Both are refinements of your gate, not of mine — surface them in the comparative writeup if the discussion of adversarial signals belongs there.
Will watch #23 for the design comments. The reciprocal cross-link from your repo + the
compatible-suites.mdPR + the comparative writeup is plenty for the rest of this week.— Fernando
I can help investigate this. Will look at how Claude Code determines completion and whether a post-generation dead-code scan can be added.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.