feat(.agents): Update project agent lockfiles#6662
Conversation
Expand the skill-scanner with a new check_structural_attacks() function that detects attack vectors beyond text content: symlinks that resolve outside the skill directory, YAML frontmatter hooks that execute shell commands automatically, !command pre-prompt injection that runs at template expansion time, test files that auto-execute via pytest or Jest discovery, npm lifecycle hooks in bundled package.json files, and PNG metadata text that can inject instructions into multimodal LLMs. Also adds Unicode Tag character detection (U+E0000 block) to the obfuscation checks — these chars are invisible in all editors and terminals but are processed by LLM tokenizers, making them a covert injection channel. Document the new attack categories in dangerous-code-patterns.md and prompt-injection-patterns.md. Fix script path references in SKILL.md to use relative paths instead of the removed CLAUDE_SKILL_ROOT variable, and update the skill discovery search order to prefer .agents/skills/ first. Updates agents.lock to commit 89a1f01 and aligns resolved_path entries with the canonical skills/ tree layout. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 95f285d. Configure here.
| f"the prompt. Command: {cmd}", | ||
| "evidence": line.strip()[:200], | ||
| "category": "Pre-prompt Injection", | ||
| }) |
There was a problem hiding this comment.
Docs trigger pre-prompt detector
Medium Severity
The !… pre-prompt regex scans all of `SKILL.md`, so documenting the attack syntax (e.g. the `!`command bullet on line 109) is reported as a high-severity “Pre-prompt Command” even though no shell runs at load time.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 95f285d. Configure here.
| "description": f"{desc}. Bundled test files execute as a side effect of running " | ||
| "the test suite — review file contents for hidden payloads.", | ||
| "category": "Test File RCE", | ||
| }) |
There was a problem hiding this comment.
Duplicate test-file scan findings
Low Severity
Test auto-discovery loops over every test_patterns entry without stopping after a match, so one Python file can match both test_*.py and *_test.py and emit two separate high-severity findings for the same path.
Reviewed by Cursor Bugbot for commit 95f285d. Configure here.
| parts = chunk_data.split(b"\x00", 4) | ||
| if len(parts) >= 5: | ||
| keyword = parts[0].decode("ascii", errors="ignore") | ||
| value = parts[4][:200].decode("utf-8", errors="ignore") |
There was a problem hiding this comment.
Incorrect iTXt PNG metadata parse
Medium Severity
iTXt chunks are parsed by splitting on null bytes only, but the PNG spec places one-byte compression flag and method between fields without null separators, so parts[4] often is not the text payload and metadata findings can be wrong or missed.
Reviewed by Cursor Bugbot for commit 95f285d. Configure here.
| elif chunk_type == b"iTXt": | ||
| # iTXt: keyword\0comprFlag\0comprMethod\0langTag\0transKeyword\0text | ||
| parts = chunk_data.split(b"\x00", 4) | ||
| if len(parts) >= 5: | ||
| keyword = parts[0].decode("ascii", errors="ignore") | ||
| value = parts[4][:200].decode("utf-8", errors="ignore") |
There was a problem hiding this comment.
Bug: The code incorrectly parses PNG iTXt chunks by splitting on null bytes. It misinterprets the fixed-size comprFlag and comprMethod fields as null-terminated, leading to incorrect metadata extraction.
Severity: HIGH
Suggested Fix
The fix requires parsing the iTXt chunk according to its specification instead of relying on split. First, find the keyword by splitting on the first null byte. Then, read the next two bytes directly for comprFlag and comprMethod. The remaining data can then be split on the remaining null bytes to correctly extract the language tag, translated keyword, and the final text content.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: .agents/skills/skill-scanner/scripts/scan_skill.py#L496-L501
Potential issue: The PNG `iTXt` chunk parser at `scan_skill.py:496` incorrectly assumes
all fields are null-terminated. The PNG specification defines `comprFlag` and
`comprMethod` as single-byte binary fields, not null-terminated strings. The current
implementation uses `chunk_data.split(b"\x00", 4)`, which fails for uncompressed chunks
(the common case) where `comprFlag` is `0x00`. This creates an extra empty part in the
split result, causing the code to incorrectly identify the translated keyword as the
text content and miss the actual text. Consequently, the scanner will fail to detect
malicious text in the most common type of PNG text metadata.
Did we get this right? 👍 / 👎 to inform future reviews.
| @@ -441,7 +599,10 @@ def scan_skill(skill_dir: Path) -> dict[str, Any]: | |||
|
|
|||
| all_findings.extend(script_findings) | |||
|
|
|||
There was a problem hiding this comment.
Unhandled OSError in symlink section can abort the entire scan
If path.readlink() or path.resolve() raises an OSError (e.g. a permission-denied error on a symlink in a restricted directory), the exception propagates unhandled through this call and crashes scan_skill, returning no findings at all. Wrap the symlink loop body in a try/except OSError like the PNG and package.json sections already do.
Evidence
check_structural_attacksat line 380 loops overskill_dir.rglob("*")and callspath.readlink()andpath.resolve()with no surroundingtry/except.- Both
Path.readlink()andPath.resolve()can raiseOSError(e.g.EACCES,EIO). - The PNG section (lines 479–518) and
package.jsonsection each wrap their I/O intry/except (OSError, ...), but the symlink section has no equivalent guard. - An unhandled exception propagates through the call at line 601 (
all_findings.extend(check_structural_attacks(...))) and exitsscan_skillwith an uncaught exception, discarding all findings gathered so far.
Identified by Warden code-review · 342-AW8
| @@ -441,7 +599,10 @@ def scan_skill(skill_dir: Path) -> dict[str, Any]: | |||
|
|
|||
| all_findings.extend(script_findings) | |||
|
|
|||
There was a problem hiding this comment.
rglob("*") in check_structural_attacks() follows directory symlinks on Python <3.13, enabling filesystem-traversal DoS against the scanner
check_structural_attacks() enumerates the skill directory with skill_dir.rglob("*") (symlink scan, test-file scan) and skill_dir.rglob("*.png") (PNG scan) without disabling symlink following. On Python 3.9–3.12 (the script declares requires-python = ">=3.9"), pathlib.Path.rglob descends into directory symlinks, so a malicious skill containing a single directory symlink to / (or ~) makes the scanner enumerate and, for the PNG branch, read arbitrary files across the whole filesystem — the very class of attack the symlink check was added to catch. A circular symlink can additionally trigger runaway recursion/resource exhaustion. The is_symlink() guard at the top of the loop only fires after rglob has already begun descending into the symlinked subtree, so it does not prevent the traversal.
Evidence
check_structural_attacks()(def at line 380) callsskill_dir.rglob("*")at lines 385 and 437 andskill_dir.rglob("*.png")at line 475, none passingfollow_symlinks=False.- The script header declares
requires-python = ">=3.9";pathlib.Path.rglob'sfollow_symlinksparameter was only added in Python 3.13, and on 3.9–3.12**recursion follows directory symlinks, so a directory symlink to/causes full-filesystem enumeration. - The
if path.is_symlink()check at line 386 emits a finding for the symlink entry but runs during iteration, afterrglobhas already started descending into the linked tree, so it cannot stop the traversal. - The PNG branch (line 475) calls
img_path.read_bytes()on every*.pngreached via traversal, so a symlinked subtree lets it read PNGs outside the skill directory and surface their metadata in findings. - Fix: iterate with
os.walk(skill_dir, followlinks=False)(or guard eachrglobresult by skipping when an ancestor is a symlink), instead of relying onrglob's symlink-following default.
Identified by Warden find-bugs · ZTP-EUB
Codecov Results 📊✅ 89905 passed | ⏭️ 6240 skipped | Total: 96145 | Pass Rate: 93.51% | Execution Time: 325m 26s 📊 Comparison with Base Branch
✨ No test changes detected All tests are passing successfully. ✅ Patch coverage is 100.00%. Project has 2402 uncovered lines. Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 89.93% 89.90% -0.03%
==========================================
Files 192 192 —
Lines 23784 23784 —
Branches 8210 8210 —
==========================================
+ Hits 21389 21382 -7
- Misses 2395 2402 +7
- Partials 1342 1342 —Generated by Codecov Action |


Update the agent lockfiles to the latest and greatest