fix(ai-detection): match binary tokens, not loose substrings - #239
Conversation
`AIToolService.detectToolFromArgs` was using `.includes('claude'|'codex'|'gemini')`
on the full `ps args=` string, with claude checked first, which mis-detected codex
(or gemini) as claude whenever the args string contained "claude" anywhere — a
quoted prompt, a worktree slug like `agent-shows-claude-not-codex`, or an install
path under `~/.claude*`.
Strict pass first: strip shell-quoted spans (single + double-quoted argument
bodies), then match each tool name only at a token boundary
(`(?:^|[\s/])name(?=\s|$)`). The original `.includes()` chain is kept as a
fallback for legacy invocation shapes the strict regex may not recognize.
Token regexes are built once at module load from `AI_TOOLS` keys, so a new tool
added to the config is detected automatically.
Adds 13 table-driven cases under `detectAllSessionAITools`: codex/gemini on a
claude-bearing slug, codex/gemini with a claude-bearing prompt (quoted by
shellQuote), codex/gemini under a claude-bearing install path, the bash-wrapper
resume/fresh shape, case-insensitive variants, and the legacy fallback path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #239: fix(ai-detection): match binary tokens, not loose substringsOverviewFixes a real, reproducible misdetection bug in Code quality and correctness
Potential issues
PerformanceNo concerns. Regexes are pre-compiled once at module load. The two sequential loops over SecurityNo new attack surface. The function reads an already-retrieved Test coverage
SummarySolid fix. Root cause is correctly identified, the implementation is minimal and doesn't over-engineer, and the test coverage directly matches the requirements. The two minor style notes (trimming the |
Trim multi-paragraph JSDoc on `detectToolFromArgs` and the `what`-describing
lines from the module-level comment block, per AGENTS.md ("Multi-paragraph
JSDoc... should be rare", "Don't explain WHAT the code does"). Keep the
load-bearing notes: the bug context, the shellQuote/quote-stripping rationale,
and the iteration-order dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #239: fix(ai-detection): match binary tokens, not loose substringsOverviewFixes a real bug where Codex/Gemini sessions were misidentified as Claude when the process args string contained the substring What's good
Issues and suggestionsLatent regex injection risk (low severity, worth a one-line guard)
new RegExp(`(?:^|[\\s/])${name}(?=\\s|$)`)All current tool names ( const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
new RegExp(`(?:^|[\\s/])${safeName}(?=\\s|$)`)Doesn't affect any current tool; future-proofs against a silent footgun. Load-order coupling comment could be sharperThe existing comment accurately describes the risk, but it points to // Iteration order is load-bearing for the loose fallback: first hit wins when args
// contains substrings of multiple tool names. If you need explicit priority, enforce
// it here rather than relying on AI_TOOLS insertion order.Unbalanced quotes in the stripped string (edge case)The quote-stripping step: argsLower.replace(/'[^']*'/g, '').replace(/"[^"]*"/g, '')… assumes balanced, non-nested quotes. If Minor:
|
Defensive: regex-escape the tool name before interpolation in TOOL_TOKEN_RES. All current tool names (claude, codex, gemini) are regex-safe, but a future addition with `.`, `+`, `(`, etc. would silently change the strict-match semantics without it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #239: fix(ai-detection): match binary tokens, not loose substringsOverviewFixes a real misdetection in Code Quality & StyleStrengths:
Style nit — comments reference the current task/branch: Per Two places violate this:
Potential Issues1. Quote stripping regex is greedy and not nested-aware (minor, low risk) const stripped = argsLower.replace(/'[^']*'/g, '').replace(/"[^"]*"/g, '');
2. The existing comment is good: // Iteration order is load-bearing for the loose fallback below: when args contains
// substrings of multiple tool names, the first hit wins.Worth extending this note to call out the specific risk: if a future tool name is a substring of another (e.g. a hypothetical tool named 3. Module-level
Test CoverageThe 13 new cases are comprehensive and cover exactly the acceptance criteria from
One case that isn't covered: what if the tool name appears in both the strict and loose positions — e.g. Security & PerformanceNo concerns. The regex matching is O(n) on a short string; the regexes are compiled once. No external input reaches any privileged operation through this path. SummaryThis is a solid, focused fix. The main actionable feedback:
Everything else looks correct, well-tested, and consistent with project conventions. ✓ |
Per AGENTS.md: don't reference the current task, fix, or callers in code comments. Trim function comment to the timeless invariant; rename test cases to short stand-alone labels and consolidate two WHY notes into one header covering shellQuote behaviour for the table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #239: fix(ai-detection): match binary tokens, not loose substringsOverviewFixes a real bug: The core logic is correct and well-tested. A few points below. IssuesMinor — comment verbosity vs. project styleAGENTS.md says: "prefer a single line … don't explain WHAT the code does." The module-level block comment on // Strict pass first: a tool name inside a prompt, slug, or install path must not
// outrank the actually-running binary. Falls back to loose `.includes()` for legacy
// invocation shapes the strict pass may not recognize.The first sentence is fine (the WHY). The second sentence explains the WHAT of the fallback, which a reader can already see in the code. Consider trimming to just the WHY. Similarly, the inline comment on // shellQuote uses single quotes for non-safe args; strip those plus double-quoted spans
// so prompt/display text can't be mistaken for a binary token.Two lines; the second line restates what the regex does. One line would suffice. Minor — the loose fallback preserves a wrong detectionThe test case Suggestion — missing ambiguous-token test caseThere's no test for args that contain two different tools as valid tokens (e.g., Strengths
SummaryApprove with the above as optional suggestions. The fix is correct, minimal, and well-tested. The comment style is the only thing that pushes against project conventions, and that's a minor point. |
Summary
AIToolService.detectToolFromArgswhere Codex (or Gemini) sessions whoseps args=contained the substringclaude— a worktree slug likeagent-shows-claude-not-codex, a prompt mentioning Claude, or an install path under~/.claude*— were rendered as Claude on the kanban / status UI.(?:^|[\s/])name(?=\s|$)); the original.includes()chain is kept as a final fallback for legacy invocation shapes. Per-tool token regexes are built once at module load fromAI_TOOLS, so a new tool added to the config is detected automatically.isAIPaneCommand, status detection, or launch logic.Test plan
npx jest tests/unit/AIToolService.test.ts— 38/38 (13 new) passnpx jest(full suite) — 78 suites, 801/801 passnpx tsc -p tsconfig.test.json— clean🤖 Generated with Claude Code