feat(skills): Add wildcard skill dependencies (name = "*")#17
Conversation
Add support for `name = "*"` in agents.toml to pull all skills from a given source, with an optional exclude list. The lockfile still enumerates each concrete skill for reproducibility. Key behaviors: - Explicit entries always win over wildcard-discovered skills - Wildcard entries support `exclude = [...]` to skip specific skills - Frozen install expands wildcards from the lockfile (no network) - Update detects new/changed/removed upstream skills for wildcards - Remove offers to add wildcard-sourced skills to the exclude list - List annotates wildcard-sourced skills with their source - Add supports `--all` flag for wildcard config creation Co-Authored-By: Claude <noreply@anthropic.com> Agent transcript: https://claudescope.sentry.dev/share/Qf-YG9fRYqU0XecGKLL_Wqjqq8U2vq-Dxvq0P8LGFFA
There was a problem hiding this comment.
🟠 [EEC-XAP] Missing error handling for resolveSkill in wildcard re-resolution path (src/cli/commands/install.ts:176) (high confidence)
When a wildcard-expanded skill needs re-resolution with a locked commit (lines 176-179), the resolveSkill call lacks try-catch error handling. In contrast, the else branch (lines 184-192) wraps the same function call in try-catch and converts errors to InstallError. If resolution fails in the wildcard path, the raw error will bubble up instead of being wrapped in a descriptive InstallError.
🟠 [937-59A] Unvalidated skillName allows potential path traversal (src/cli/commands/remove.ts:98) (medium confidence)
The skillName from command-line args (line 98) is used directly in join(skillsDir, skillName) (lines 41, 118) without validation against skillNameSchema. A malicious input like ../../../etc could construct paths outside the intended skills directory, though rm would only succeed if the path exists and has appropriate permissions.
src/cli/commands/update.ts:121) (high confidence)
validateTrustedSource is called without the config.trust parameter, causing it to return immediately and skip all trust checks. Compare to install.ts:167 which correctly passes config.trust.
src/skills/resolver.ts:164) (high confidence)
The resolveWildcardSkills function uses d.meta.name from SKILL.md files without validating against skillNameSchema. A malicious repo could contain name: "../../../malicious" in SKILL.md, causing join(skillsDir, name) in install.ts to write files outside the skills directory.
Identified by Warden via find-bugs
- Pass config.trust to update helper functions (was silently allowing all sources by omitting the trust parameter) - Use lockfile for concrete skill names in remove gitignore update (literal "*" from wildcard entries was matching all skills in gitignore) - Track wildcard removals separately so lockfile is persisted even when the only changes are upstream skill deletions Co-Authored-By: Claude <noreply@anthropic.com> Agent transcript: https://claudescope.sentry.dev/share/XlKtRbKngflaSdn-1aoADpuokeN_YlukhiVCnE4G9vQ
- Validate discovered skill names against safe regex in resolveWildcardSkills to prevent path traversal via malicious SKILL.md frontmatter - Trim TOML lines before regex matching in addExcludeToWildcard to handle valid indented TOML - Return removed skills from runUpdate so the CLI reports upstream removals instead of silently saying "up to date" - Make sync command wildcard-aware: expand wildcards from lockfile for declared names and gitignore, skip "*" in missing skills check Co-Authored-By: Claude <noreply@anthropic.com> Agent transcript: https://claudescope.sentry.dev/share/tL6yIUATV96GpNKi9FF4unQ2yGyU2Kta5qyrSx08c0A
| if (!discoveredNames.has(name) && !excludeSet.has(name)) { | ||
| delete newLock.skills[name]; | ||
| await rm(join(skillsDir, name), { recursive: true, force: true }); | ||
| removed.push(name); | ||
| } |
There was a problem hiding this comment.
Bug: The skill removal logic in update.ts incorrectly checks !excludeSet.has(name), preventing cleanup of skills that are manually added to the exclude list.
Severity: MEDIUM
Suggested Fix
Remove the !excludeSet.has(name) check from the if condition in src/cli/commands/update.ts. The condition should simply be if (!discoveredNames.has(name)) to ensure that any skill no longer discovered (either removed upstream or excluded) is properly cleaned up.
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: src/cli/commands/update.ts#L230-L234
Potential issue: When a user manually adds a skill to the `exclude` list in
`agents.toml` for a wildcard source and then runs `dotagents update`, the skill is not
removed from the filesystem or the lockfile. The condition `if
(!discoveredNames.has(name) && !excludeSet.has(name))` in `update.ts` is incorrect. The
`!excludeSet.has(name)` check prevents the removal of a skill that is explicitly
excluded, leaving orphaned files and stale lockfile entries.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| if (wildcardSources.has(locked.source)) { | ||
| declaredNames.add(name); | ||
| } | ||
| } |
There was a problem hiding this comment.
Sync omits wildcard exclude list filtering from lockfile
Medium Severity
When building declaredNames from the lockfile for wildcard sources, sync.ts doesn't filter by the wildcard's exclude list. Both install.ts (frozen mode, line 93) and list.ts (line 52) correctly build an excludeSet from wDep.exclude and skip matching names, but sync.ts adds every lockfile entry whose source matches a wildcard source without any exclude check. This causes excluded skills still present in the lockfile to be incorrectly treated as "declared," leading to spurious "missing skill" warnings.
| resolved = await resolveSkill(name, dep, { | ||
| projectRoot: scope.root, | ||
| lockedCommit, | ||
| }); |
There was a problem hiding this comment.
Missing try-catch on wildcard skill re-resolve path
Low Severity
When a wildcard-expanded skill needs re-resolution with a locked commit (because the fresh commit differs from the lockfile), the resolveSkill call at line 176 lacks a try-catch, unlike the equivalent call for regular skills at line 184. If this resolution fails, a raw ResolveError propagates instead of a user-friendly InstallError, which the CLI handler doesn't catch — resulting in an unhandled error with a stack trace rather than a clean error message.


Add support for
name = "*"inagents.tomlto install all skills from a source with a single entry, replacing the need to enumerate each skill individually.The lockfile continues to enumerate each concrete skill for reproducibility. Wildcard expansion happens at install/update time — frozen installs expand from the existing lockfile with no network access.
Key behaviors
name = "code-review"andname = "*"resolve from the same repo, the explicit entry takes precedenceexclude = [...]on a wildcard skips specific skills(* source)--all—dotagents add getsentry/skills --allcreates a wildcard entryFiles changed
schema.ts,loader.ts,writer.ts,index.tsaddWildcardToConfig,addExcludeToWildcard, duplicate-source validationresolver.ts,index.tsresolveWildcardSkills()discovers all skills from a sourceinstall.tsexpandSkills()helper for wildcard expansion with frozen/force supportupdate.tsupdateWildcardSource()with new/changed/removed detectionadd.ts--allflag for wildcard config creationremove.tsWildcardSkillRemoveError, interactive exclude promptlist.tsCo-Authored-By: Claude noreply@anthropic.com
Agent transcript: https://claudescope.sentry.dev/share/uGvevEfl0wIbQi9loepf7YDoHBIZqWsB0a6T3Uwen3w