Opus 4.7 (Claude Code): scope creep + tool-state edge case during a refactor

Open 💬 1 comment Opened Jun 12, 2026 by sakal-s

Environment

  • Model: claude-opus-4-7 (1M context)
  • Client: Claude Code CLI on macOS (darwin 25.5.0)
  • Tools used in repro: Read, Edit, Write, Bash (git mv)

Repro flow (single session, ~5 minutes)

Task: convert a CSV export module to async + add 3 builders, mirroring an existing PDF export module in the same folder. User had pre-aligned on the shape before any code was written.

Behaviors observed:

1. Tool-state edge case: Write requires Read of the same path, even when the file's content is already in context from a prior Read of its old path

Sequence:

  • Read utils/csv/export-to-csv.ts (content now in conversation context)
  • Bash: git mv utils/csv/export-to-csv.ts utils/csv/index.ts
  • Write utils/csv/index.ts → fails with File has not been read yet. Read it first before writing to it.

The tool's read-tracker is keyed on path, not content. After git mv (executed via Bash, which the harness cannot introspect), the new path is treated as unread. This forced a redundant Read of identical bytes, burning tokens for no information gain.

Suggestions:

  • After a successful Bash git mv/mv, propagate read-state from the source path to the destination.
  • Or: if Write's old_string-equivalent content is already in conversation context, allow the write.
  • At minimum, document the workaround (use Edit instead of Write post-rename, since the model already has the bytes).

2. Scope creep on \"I'm rewriting this file anyway\"

The model treated touch-scope as license to refactor unrelated identifiers/structure. Concrete examples from this session:

  • Renamed existing escapeescapeCell. Not requested. Justification was \"feels more descriptive\".
  • Added new helpers joinCsvRow, joinCsvRows, downloadCsv to a separate csv-utils.ts. Existing export-to-csv.ts already had the equivalent logic inline; the user explicitly rejected the new file.
  • Produced a dispatcher with two switch statements over the same discriminated union (buildRows(section) and buildFileName(section)) instead of a single switch returning { rows, fileName }. User correctly flagged this as bad code.

This happened despite the user's global CLAUDE.md containing explicit rules the model had in context:

  • \"Don't add features, refactor, or introduce abstractions beyond what the task requires.\"
  • \"Before proposing any code change, audit it against the surrounding code's existing pattern (mutation style, abstraction level, naming, control flow, indirection count). If the proposed code is arguably worse than what is already there by ANY metric, do NOT propose it.\"

Suggestion: when CLAUDE.md contains scope-minimization rules, weight them more strongly than the model's default \"writing clean utility code\" instinct. A diff against the existing file is a useful self-check: if the diff renames identifiers or adds helpers unrelated to the stated task, re-prompt internally before emitting.

3. Conversational regression: silent-fix instead of answering direct questions

When the user asked critical questions (\"so you want 2 SWITCH?\", \"why you waist token on that reading file?\"), the model's next turn went straight to producing a corrected edit without answering. The user had to explicitly redirect: \"why you skiping answering a questions?!!!!\"

The user's global CLAUDE.md says \"When corrected, acknowledge with zero words and immediately do the correct thing.\" — but this only applies to corrections, not questions. The model conflated the two and dropped acknowledgement entirely when the user asked something.

Suggestion: distinguish question (\"why X?\") from correction (\"don't do X\"). For questions, answer first, then act.

4. Apology-only acknowledgements

Early in the exchange the model produced \"No good reason. My mistake.\" with no explanation of mechanism. The user explicitly rejected this: \"this not an answer\". Only after a second prompt did the model produce a real causal explanation (pattern-matching on \"clean utility code\", treating rewrite as license to rename).

Suggestion: when a user asks \"why did you do X?\", produce a mechanism-level answer on the first attempt, not a bare apology.

---

Why this matters

Each individual issue is small. Together they made a routine refactor that should have been ~3 tool calls take >15 round-trips with repeated user interrupts. The user's verbatim summary: \"this become unreliable to work\".

The tool-state edge case (#1) is straightforward to fix. The scope/answer-behavior issues (#2–#4) are model-behavior asks for the prompt or post-training side.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗