Skip to content

fix(claude-ops): leave a built-in name unresolved when it is bound to a non-constant expression - #5704

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/5700-inventory-nonconstant-name
Oct 1, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
fix/5700-inventory-nonconstant-name

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #5700
Closes #5701

Summary

On Claude Code 2.1.286 the inventory reported a built-in command string that does not exist. The generic skill loader builds {type:"prompt",name:Vt,...} with Vt=$t?smt(e):e in its own scope. The name resolver searched an index that holds only string bindings, so it could not see that nearer conditional binding and picked an unrelated Vt="string" megabytes earlier. That is a wrong value. The reader is supposed to fail closed: a name it cannot resolve stays unresolved.

Fix

  • Before a name candidate is accepted, _scoped_constant checks it with the module and scope rule that field resolution uses (_binding_value). The candidate must be the string constant that this read sees. A name bound to a conditional, a call or any other non-constant expression stays unresolved, and so does a name whose constant lives in another module. The check applies to commands, bundled skills, subagents and tools: resolve_name_ident and resolve_tool_ident now take src and braces.
  • _binding_value gains a window argument, so a single-character name keeps the existing SHORT_IDENT_LOCALITY_BYTES reach.
  • VALIDATED_AGAINST is now 2.1.286. claude-ops is bumped to 0.79.2 with a CHANGELOG entry, and reference/extraction.md describes the check.

Verification

  • Full --binary-only inventories were diffed before (origin/main) and after the fix:
    • 2.1.285: no surface changes. Only the validated_against advisory text differs.
    • 2.1.286: builtin_commands.string is removed and string drops out of integrity.undetermined. Nothing else changed in any lane. No other phantom of this bug class turned up.
  • 2.1.285 to 2.1.286 after the fix: every lane has the same names. The field changes are pause-memory (its description is now a getter), plugin-types (now hidden) and Explore/Plan disallowed_tools. The Explore/Plan lists are partial because the ...pY spread hits a nearer pY=p(...) call. That is fail-closed (the list is a floor), and this PR leaves it alone.
  • inventory.py --self-check on the installed CLI: OK: cli 2.1.286, validated against 2.1.286, all six lanes ok. Eval-backed facts hold on 2.1.286: usage aliases cost/stats, heapdump/sandbox hidden, teleport hidden and gated, vim removed_in_docs.
  • test_inventory.py: 209 OK. New fixtures cover a name shadowed by a conditional (the 2.1.286 shape), by a call, by a non-constant in a bundled skill and in a tool, a constant in another module, and a same-scope snake_case rebinding of a tool. Positive controls cover an in-scope constant and an imported one. With the guard stubbed out, 6 of the new tests fail.
  • test_overlap.py 184 OK, overlap.test.sh passes, test_native_drift.py 40 OK, native_drift.test.sh passes, and scripts/run-ruff.sh check and format --check are clean.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 1, 2026 10:15
… a non-constant expression

The name resolvers took the nearest preceding string constant from an
index that holds only string bindings, so a nearer binding to a
conditional or call (the 2.1.286 skill loader's `Vt=$t?smt(e):e`) was
invisible and an unrelated `Vt="string"` far ahead became a phantom
built-in command. Every candidate now must be the string constant the
module and scope rule says the read sees; otherwise the name stays
unresolved. Revalidated against 2.1.286.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 1, 2026 14:17
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T14:20:47.659095Z e41ecf9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 59s —— View job


Code review complete

  • Gather context (PR diff, changed files)
  • Review inventory.py changes (_scoped_constant, _binding_value, resolve_name_ident, resolve_tool_ident)
  • Review test_inventory.py new fixtures
  • Review docs/CHANGELOG/plugin.json bump
  • Post findings

No blocking findings.

Summary

Traced the new _scoped_constant gate end-to-end:

  • resolve_name_ident and resolve_tool_ident correctly thread src/braces through every call site (inventory.py#L2320, #L2814, #L3245, #L3402, #L3510).
  • _binding_value's new window param defaults to the pre-existing SHORT_VALUE_LOCALITY_BYTES, so the three untouched call sites (#L2039, #L2114, #L3013) are unaffected; _scoped_constant passes SHORT_IDENT_LOCALITY_BYTES, matching the locality already enforced just above it in both resolvers for single-character names.
  • resolve_tool_ident's rewrite (#L3155-3181) preserves the original "nearest PascalCase wins, no further fallback" selection and only adds validation on top — no behavior change for the selection itself, consistent with the fail-closed design.
  • _CONST_VALUE_RE's lookahead (,;)}\n or end-of-string) is narrower than the full set of valid JS statement terminators (e.g. a binding inside [...] or followed by &&), but any miss there just leaves a name unresolved rather than producing a wrong value — in line with the PR's explicit fail-closed intent, not a correctness bug.
  • test_inventory.py's TOOL_SRC fixture change (moving the decoy k1="system_assigned_identity" rebinding inside function Ku(){...}) is a deliberate and correct update: under the new scope-aware check, a module-top-level decoy would now (correctly) make SendUserFile unresolved, so the fixture needed to move the decoy out of the reader's visible scope to keep testing the PascalCase-preference behavior in isolation.
  • Docs (extraction.md) and CHANGELOG.md accurately describe the fix and match the code.

One pre-existing, untouched code path worth noting for awareness only (not a finding against this PR): in extract_builtin_tools, a single-character tool-name identifier that fails to resolve is silently counted under factory_definitions rather than unresolved_names (#L3513-3516). The stricter _scoped_constant check could make this trigger more often on real builds than before, but that logic itself isn't touched by this diff, and the author's verification section reports a full binary diff against 2.1.285/2.1.286 showing no unexpected surface changes, so there's no concrete evidence this manifests.

Verification note: I was unable to execute pytest/unittest or ruff in this sandbox (commands required interactive approval that wasn't available), so the test and lint results in the PR description are author-claimed and unverified by me. Everything above is from static reading of the diff.

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 0s —— View job


Security review — /review:security-review

  • Gather context (PR diff, changed files)
  • Run /review:security-review criteria over the diff
  • Post findings or report clean

Scope reviewed (diff against origin/main, head e41ecf9):

  • plugins/claude-ops/skills/inventory/scripts/inventory.py
  • plugins/claude-ops/skills/inventory/scripts/test_inventory.py
  • plugins/claude-ops/skills/inventory/reference/extraction.md
  • plugins/claude-ops/CHANGELOG.md, plugins/claude-ops/.claude-plugin/plugin.json

No security issues found.

This change tightens a static-analysis heuristic (_scoped_constant in
inventory.py)
used by the inventory skill's read-only parser for the locally installed Claude Code CLI bundle.
Relevant observations from the review:

  • No new I/O, subprocess, eval/exec, network, or filesystem-write paths were introduced — the
    diff is confined to regex-based name/constant resolution over an already-loaded source string
    and its accompanying tests/docs.
  • The new _CONST_VALUE_RE and the window parameter on _binding_value don't introduce
    catastrophic-backtracking risk: _STR ("((?:[^"\\]|\\.)*)") has no nested ambiguous
    quantifiers, and the added lookahead is a small bounded character class.
  • No instruction-surface deletions: the diff only adds documentation/guardrail text (extraction.md,
    CHANGELOG.md) and makes name resolution stricter (fail-closed), narrowing nothing a control
    depends on.
  • No secrets, tokens, or credentials are touched; the tool only reads a locally installed binary
    to build a report.

This PR has no security-relevant surface change, so per the skill's skip gate there's nothing
further to flag here. (Logic/correctness review, if wanted, is out of scope for this lane —
/review:code-review covers that.)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e41ecf9926

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/inventory/reference/extraction.md
@kyle-sexton
kyle-sexton enabled auto-merge (squash) October 1, 2026 14:48
@kyle-sexton
kyle-sexton merged commit d0277f5 into main Oct 1, 2026
22 of 31 checks passed
@kyle-sexton
kyle-sexton deleted the fix/5700-inventory-nonconstant-name branch October 1, 2026 14:50
kyle-sexton added a commit that referenced this pull request Oct 1, 2026
… in its own scope (#5717)

Closes #5711

## Summary

On Claude Code 2.1.286 the inventory read the built-in Explore and Plan
agents' `disallowedTools` as `partial`. Both definitions spread a shared
list (`...pY`). The spread resolver took the nearest `pY=` binding
anywhere in the bundle, which on 2.1.286 is an unrelated `pY=p(...)`
call, so the shared entries, the Artifact tools among them, were
dropped.

## Fix

- `_array_names` resolves a `...spread` element through a new
`_spread_array` helper. It reads the binding with `_binding_value` at
the spread's own offset, the same module and scope rule
`_scoped_constant` applies to names and fields (#5619, #5704). Before,
it used `_nearest_binding`.
- Fail-closed:
- When the scoped binding is not an array literal (a call, a
conditional, or undeterminable), the list stays `partial`.
- When the selected binding is a bare or conditional assignment rather
than a declaration (`if(c)pY=["B"]`, `pY=c?["B"]:pY`, `c&&(pY=["B"])`,
an unbraced `for(...)pY=["B"]`), or any other write in the same block
reaches it, the list stays `partial`. A `var` reached back through a
chain of simple declarators counts as a declaration, which keeps Explore
and Plan resolving: their `pY` follows a function declaration that has
no `;`, and `_declares` misses that statement.
- When code off the declaring block's straight line assigns the binding
without declaring its own, the list also stays `partial`. That code is a
nested block (`if(c){pY=["B"]}`, a loop body), another function
(`function init(){pY=["B"]}`), or an expression-bodied arrow (`var
f=()=>pY=["B"]`, in either order relative to the declaration). A read
can then see B, so the list is not static. This check
(`_written_elsewhere`) applies only to the spread path. Applied inside
`_binding_value`, it changed many unrelated description fields on both
2.1.285 and 2.1.286 and made the run about 5x slower, so it is not
shared with name and field resolution.
- Tests in `test_inventory.py`:
- A spread whose nearest same-name binding is local to another function
resolves to the in-scope array. This test fails on the unfixed resolver.
  - A spread whose in-scope binding is a conditional stays `partial`.
- A spread whose binding another function, a nested `if` or loop block,
or an expression-bodied arrow reassigns stays `partial`.
- A writer that declares its own local of the same name does not block
resolution.
- `reference/extraction.md` states the spread rule. claude-ops 0.79.2 ->
0.79.3, with a CHANGELOG entry.

## Verification

- `inventory.py --binary-only` on 2.1.286, diffed against the prior run
(`.work/cc-2.1.286/inv-final.json`). Only these fields changed:
- `builtin_agents/Explore/disallowed_tools`: `[Agent, ExitPlanMode,
Edit, Write, NotebookEdit]` -> `[Agent, Artifact, ArtifactComments,
ArtifactData, ArtifactCheck, ExitPlanMode, Edit, Write, NotebookEdit]`
- `builtin_agents/Explore/disallowed_tools_source`: `partial` ->
`literal`
- `builtin_agents/Plan/disallowed_tools` and `disallowed_tools_source`:
the same change
- On 2.1.285, the inventory output is identical to origin/main's.
- The reassignment checks (commits f88d9bf and 117a3fb) left the 2.1.285
and 2.1.286 outputs identical to the first fix commit (3075883), and
runtime is unchanged (about 22s).
- `inventory.py --self-check`: `OK: cli 2.1.286, validated against
2.1.286`, with all six lanes ok.
- `python3 -m unittest test_inventory`: `Ran 215 tests ... OK`.
- `overlap.py detect`: exit 0, with `discovered: 0` and `resurfaced: 0`.
`overlap.py generate --check` reports the docs are in sync. `node
scripts/generate-catalog.mjs` produced no diff.
- `scripts/run-ruff.sh check` and `format --check` pass on both files.

## Related

- #5640: move to a real JavaScript parser, which would replace these
scope heuristics.
- #5704 / #5700: the same scoped-binding rule, applied to names.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Oct 1, 2026
…e Code 2.1.287 (#5731)

Closes #5730

## Summary

Per-release native-surface pass for Claude Code 2.1.287. Every inventory
lane extracts ok on 2.1.287, so the inventory is revalidated against it.
The one native surface that moved is the hidden built-in command
`/plugin-types`, which the 2.1.287 build no longer ships. Its dismissal
against `code-metrics:audit-type-debt` was orphaned and is removed.

## Fix

- `VALIDATED_AGAINST` in
`plugins/claude-ops/skills/inventory/scripts/inventory.py` is now
`2.1.287`.
- The `plugin-types` / `code-metrics:audit-type-debt` dismissal is
removed from `docs/native-surfaces/records.json`. `overlap.py` has no
undismiss command, so the record was deleted from the store and
`docs/native-surfaces.md` was regenerated with `overlap.py generate`.
`node scripts/generate-catalog.mjs` reported the catalog already in
sync.
- claude-ops is bumped from 0.80.0 to 0.80.1, with a CHANGELOG entry.

## Verification

- Binary string search, not variable names: in 2.1.286, `/plugin-types`
occurs 10 times and `Write claude-code.d.ts` 2 times. In 2.1.287 both
occur 0 times. The only `plugin-types` left in 2.1.287 is the CSP
directive name inside a list of `*-src` directives.
- Surface diff, 2.1.286 final extraction against 2.1.287:
builtin_commands went from 111 to 110 (`plugin-types` removed). Nothing
was added or renamed in any lane. The Explore and Plan
`disallowed_tools` now read `literal` with the Artifact tools included.
That comes from the extractor fix in #5717, not from a change in the
binary.
- `inventory.py --self-check`: `OK: cli 2.1.287, validated against
2.1.287`, all six lanes ok, exit 0. It was `DEGRADED` (exit 3) before
the bump.
- `overlap.py detect` on the final extraction: exit 0, integrity ok,
discovered 0, resurfaced 0, orphaned dismissals 0, 95 suppressed.
- `overlap.py self-check`: exit 3, degraded only by the 2 standing
advisories it also reports on main (older recorded extraction versions
on rows, upstream SHA not locally decidable). 69 rows checked, 0
problems.
- `test_inventory.py`: 215 tests OK. `test_overlap.py`: 184 tests OK.
Pinned ruff passes on `inventory.py`.

## Related

- #5704: the same pass for 2.1.286.
- #5717: the Explore and Plan disallowed-tools extractor fix.
- #5730: the native-drift item
`native-drift:inventory-degraded:2.1.287:inventory`, filed by this pass
and closed by this PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

claude-ops/inventory: extraction degraded on Claude Code 2.1.286 claude-ops/inventory: resolve the string description the extraction left unresolved

1 participant