Skip to content

fix(anthropic): align Glob/Grep with the real Claude Code schema - #4

Open
st333in wants to merge 1 commit into
dwgx:masterfrom
st333in:fix/grep-glob-claude-code-schema
Open

fix(anthropic): align Glob/Grep with the real Claude Code schema#4
st333in wants to merge 1 commit into
dwgx:masterfrom
st333in:fix/grep-glob-claude-code-schema

Conversation

@st333in

@st333in st333in commented Aug 26, 2026

Copy link
Copy Markdown

Summary

Four defects in the Glob/Grep mapping between Claude Code and Kiro. Three of them make the client reject the entire tool call; the fourth fails silently, which makes it the dangerous one.

# Defect Symptom
1 Grep leaks explanation outbound InputValidationError: ... An unexpected parameter \explanation` was provided` — the whole tool call is rejected
2 Glob leaks includeIgnoredFiles outbound same failure mode
3 Grep fabricates exclude and case_sensitive outbound remap_tool_keys renames into key names that do not exist in Claude Code's schema
4 -i has both the wrong key name and inverted polarity silent: a case-insensitive search ran case-sensitive, returned nothing, and the empty result was indistinguishable from "the text does not exist"

On defect 4: Claude Code sends ripgrep-style -i (case insensitive = true), while the inbound mapping only read caseSensitive (case sensitive = true). Different name, opposite meaning, so the request to ignore case was dropped and the model drew the wrong conclusion from the empty result.

The fix advertises Claude Code's real schema instead of translating, which collapses the mapping to near-identity and removes the whole class of bug.

Motivation and background

kiro_builtin_tool_schema advertises Kiro-native parameter shapes upstream, and a bidirectional layer then translates them back into Claude Code shapes. I measured this against a live proxy with a small client posting to a local /v1/messages, and Kiro accepts and honours an arbitrary declared schema: advertise the Claude Code shape and the model emits the Claude Code shape directly.

That makes the translation layer a net loss rather than a necessity.

It is the only source of the phantom parameters. Defects 1–3 are all produced by it. This failure has now recurred twice in the wild — explanation on Grep, includeIgnoredFiles on Glob — and each occurrence costs the entire tool call, not just the offending argument.

It amputates real client capability. path, type, output_mode, head_limit, multiline, -n and -A/-B/-C were never advertised, so the model had no way to know they exist. path matters most: per the official tools reference, Grep respects .gitignore, and the prescribed way to search an ignored path is to pass that path directly. Without path that escape hatch is unreachable. In testing, the model could only cram the directory into glob, and node_modules stayed invisible in a way it could not distinguish from an absent file.

Exclusion needed no translation in the first place. Claude Code's glob maps to rg --glob, and ripgrep supports ! negation, so once the real schema is advertised the model writes !**/*.rs unprompted.

Schemas were taken from the official tools reference, Glob and Grep sections.

Type of change

  • feat
  • fix
  • docs
  • refactor
  • perf
  • test
  • chore / build / ci

Testing

  • cargo test passes
  • cargo fmt applied
  • cargo clippy reports no new warnings
  • pnpm build (not applicable — no frontend changes)
  • manually verified the affected scenarios

cargo test --no-default-features: 2288 passed / 0 failed.

cargo clippy --no-default-features: no new warnings. Four warnings remain in tool_compat.rs and are pre-existing, in code this PR does not touch: maybe_insert, remap_tool_keys, the map_tool_name doc list, and the unused restore_tool_use_for_client re-export.

Beyond the unit tests, each scenario below was measured against a running proxy carrying this build, using the builtin tool names Claude Code actually sends. The assertion is that every returned key fits Claude Code's real schema:

Scenario Returned tool_use.input
search inside a gitignored directory {"pattern":"license","path":"admin-ui/node_modules","glob":"**/package.json","output_mode":"files_with_matches"}
case-insensitive plus line numbers {"pattern":"usestate","-i":true,"-n":true,"head_limit":20,"output_mode":"content"}
exclusion {"pattern":"TODO","glob":"!**/*.rs","output_mode":"files_with_matches"}
Glob scoped to a directory {"pattern":"**/*.toml","path":"KiroStudio-src"}

Zero phantom parameters across all four. path now arrives, the dash on -i/-n survives, and exclusion comes out native.

The existing regression test was extended to cover the outbound allowlist, -i polarity in both directions, the bare-i to -i dash restoration, and path no longer being dropped.

Checklist

  • Commit message follows Conventional Commits
  • Tests added for the fixed defects
  • No new dependencies
  • Documentation updated (not needed)
  • No secrets, credentials or sensitive data committed

Additional notes

A finding worth flagging on its own. The declared keys are -i and -n, but the model returns them without the leading dash, as i and n. This repository is not the cause — normalize_json_schema does not rewrite property names — so the dash is dropped either upstream or by the model. Left alone this would have been the third recurrence of the same phantom-parameter failure, so the outbound path now restores bare i/n/A/B/C to their dashed form. Worth knowing if any other dash-prefixed parameter is ever advertised.

Scope. Limited to Glob and Grep, the two tools whose schema I could verify end to end. The other six builtin tools keep the existing "preserve unmapped keys" contract, which Write.write_mode depends on and an existing test pins.

Behavioural change to be aware of. This changes the schema surface the model sees, which changes the parameter shapes it generates. If there is a reason to keep the Kiro-native shape that I am not seeing — prompt cache stability, or parity with the reference implementation the comments mention — a narrower version that only strips the leaked keys is easy to derive from this one, though it would leave defect 4 and the missing path unfixed. Happy to split it that way.

Verification I could not run. CLAUDE.md prescribes the skiapi Docker loop as the verification path. I do not have access to that host, so the numbers above come from a local build plus the live-proxy measurements.

Note for anyone reproducing on Windows. With core.autocrlf=true, seven tests fail locally that pass in CI. They include_str! their own source and search for LF-only literals such as "\n}\n", which cannot match in a CRLF working tree. The 2288/0 figure above is from an LF checkout, matching CI. Unrelated to this PR, but it costs time to rediscover.

Claude Code rejects the whole tool call with
`InputValidationError: ... An unexpected parameter \`X\` was provided`,
and separately drops a case-insensitivity request without any error.
Four defects, one shared root cause.

## Defects

1. Grep leaked `explanation` outbound. Claude Code's Grep schema has no such
   key. The former comment claimed "explanation is required", which the
   official tools-reference contradicts.
2. Glob leaked `includeIgnoredFiles` outbound. Claude Code's Glob only takes
   {pattern, path}; gitignore behaviour is controlled by the launch-time
   CLAUDE_CODE_GLOB_NO_IGNORE environment variable, not a per-call parameter.
3. Grep *fabricated* `exclude` and `case_sensitive` outbound: the target key
   names passed to remap_tool_keys do not exist in Claude Code's schema at all.
4. `-i` had both the wrong key name and inverted polarity, and failed
   silently. Claude Code sends ripgrep-style `-i` (case *insensitive* = true),
   while the inbound mapping only read `caseSensitive` (case *sensitive* =
   true). The request to ignore case was discarded, the search ran
   case-sensitive, and the empty result was indistinguishable from "the text
   does not exist" - so the model concluded exactly that.

## Root cause and fix

`kiro_builtin_tool_schema` advertised Kiro-native shapes upstream and relied
on a bidirectional translation layer to restore Claude Code shapes on the way
back. Measured against a live proxy (custom client posting to a local
/v1/messages), Kiro **accepts and honours an arbitrary declared schema**:
advertise the Claude Code shape and the model emits the Claude Code shape
directly.

That makes the translation layer a net loss rather than a necessity:

- It is the sole source of the phantom parameters. Defects 1-3 are all
  produced by it, and leaking a single one costs the entire tool call.
- It amputates real client capabilities. `path` (including the escape hatch
  the official docs prescribe - pass a gitignored path directly to search
  inside it), `type`, `output_mode`, `head_limit`, `multiline`, `-n` and
  `-A/-B/-C` were never advertised, so the model had no way to know they
  exist. In testing the model could only cram the directory into `glob`, and
  since Grep respects .gitignore, node_modules stayed unreachable and
  indistinguishable from an absent file.

Now the real Claude Code schema is advertised directly, the mapping collapses
to near-identity, and legacy key aliases plus an outbound allowlist remain as
a safety net. The allowlist encodes what is permitted rather than what is
forbidden, so a newly introduced phantom key is blocked automatically.

Exclusion needs no translation at all: Claude Code's `glob` is `rg --glob`, so
with the real schema advertised the model writes `!**/*.rs` on its own.

## One finding that only testing surfaced

The declared keys are `-i` and `-n`, but the model returns them **without the
leading dash** as `i` and `n`. This repo is not the culprit -
normalize_json_schema does not rewrite property names. Left alone this would
be the third recurrence of the same phantom-parameter failure, so the outbound
path now restores bare `i`/`n`/`A`/`B`/`C` to their dashed form.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant