Skip to content

feat(cursor): discover FS skills and apply $mentions on send#4297

Closed
blaqat wants to merge 5 commits into
pingdotgg:mainfrom
blaqat:feat/cursor-fs-skill-discovery
Closed

feat(cursor): discover FS skills and apply $mentions on send#4297
blaqat wants to merge 5 commits into
pingdotgg:mainfrom
blaqat:feat/cursor-fs-skill-discovery

Conversation

@blaqat

@blaqat blaqat commented Jul 22, 2026

Copy link
Copy Markdown

Summary

  • Discover Cursor skills from <cwd>/.cursor/skills/**/SKILL.md and ~/.cursor/skills/**/SKILL.md into the provider $ menu
  • On send, expand $skill mentions by injecting SKILL.md bodies into the ACP prompt (Cursor has no native $ interpreter)
  • Harden: symlink containment, leading $ token boundary, 64KiB body cap + frontmatter strip
  • Listing uses ServerConfig.cwd; send-apply merges session cwd + server cwd

Commits (5)

  • 7173f10a3 feat(cursor): discover FS skills and apply $mentions on send
  • 95e17c1c7 fix(cursor): discover FS skills using ServerConfig.cwd
  • 44c3e9ad4 fix(cursor): apply listed skills when session cwd diverges
  • 82c445ede fix(cursor): harden FS skill discovery before ship
  • 7678e7bbf docs(plans): mark Cursor FS skill Phase 1 done

Test plan

  • Select Cursor provider, open $ — project/user skills appear
  • Select a skill, send a message — model receives skill content (not inert $name)
  • Confirm ENV=$foo does not trigger skill expand
  • Symlink escape skills are not listed/applied
  • pnpm focused tests: cursorSkillDiscovery, CursorProvider, CursorAdapter

Notes

  • OpenCode / ACP / slash commands deferred
  • Per-thread $ menu still process-wide (ServerConfig.cwd)

Made with Cursor

Note

Discover filesystem Cursor skills and inject $mention content into prompts on send

  • Adds a new cursorSkillDiscovery module that reads .cursor/skills/SKILL.md files from the session cwd and ServerConfig.cwd, enforcing size caps, symlink containment, and name validation.
  • makeCursorAdapter now runs skill discovery and calls applyCursorSkillMentions in sendTurn, replacing $skill tokens with the corresponding SKILL.md body before sending to Cursor ACP.
  • checkCursorProviderStatus accepts a new cwd argument and includes discovered skills in the returned ServerProviderDraft; callers must now supply this argument.
  • Behavioral Change: CursorDriver.create passes serverConfig.cwd to checkCursorProviderStatus, changing where skill discovery is rooted from the implicit process cwd to the configured workspace directory.
📊 Macroscope summarized 7678e7b. 5 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

blaqat and others added 5 commits July 22, 2026 14:13
Populate Cursor provider snapshot.skills from project/user .cursor/skills
SKILL.md files, and inject matched skill bodies in CursorAdapter.sendTurn
so Codex-style $name tokens are useful under ACP (which has no $ interpreter).

Co-authored-by: Cursor <cursoragent@cursor.com>
Align snapshot `$` skill listing with send-path apply by scanning
project skills under the active workspace cwd instead of process.cwd().

Co-authored-by: Cursor <cursoragent@cursor.com>
Provider `$` listing is process-wide from ServerConfig.cwd, while send used
only session cwd. Merge both project roots on send-apply so menu-listed
skills still inject when a thread/worktree cwd differs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reject symlink escapes via realpath/lstat containment, require a leading
boundary on $skill mentions so ENV=$name is not expanded, and cap injected
SKILL.md bodies (strip frontmatter, 64KiB) at discover and apply.

Co-authored-by: Cursor <cursoragent@cursor.com>
Record beach-mode gate status, shipped SHAs, and residuals
(per-thread menu, OpenCode, ACP slash) before push.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0af83b15-c3a1-4e54-91f3-050f07a60d1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 22, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.

Effect.provideService(Path.Path, path),
Effect.orElseSucceed(() => [] as const),
);
const promptText = applyCursorSkillMentions(input.input.trim(), discoveredSkills);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Scans skills on every send

Medium Severity

Every sendTurn with non-empty text unconditionally calls discoverCursorSkills, scanning project and user skill directories. This occurs even when no $ skill mention is present in the input, adding unnecessary filesystem I/O and latency to most turns.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.

discoveredModels = discoveryExit.value;
}
}
// Soft-fail FS skill discovery for the `$` menu snapshot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probe early exit drops skills

Medium Severity

checkCursorProviderStatus only runs filesystem skill discovery after the parameterized model-picker gate. When that check fails (for example non-lab CLI channel or an older Agent version), the function returns early with default empty skills, so the composer $ menu stays empty even when .cursor/skills files exist on disk.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.

if (!realSkillPath || !isPathInsideRoot(realSkillsRoot, realSkillPath)) {
continue;
}
const raw = yield* readFile(realSkillPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium provider/cursorSkillDiscovery.ts:390

discoverCursorSkills calls fileSystem.readFileString to read each SKILL.md entirely into memory, and only afterward does parseCursorSkillMarkdown enforce the 64 KiB cap. A multi-megabyte SKILL.md under a scanned skills root is therefore fully materialized on every invocation before being rejected, so the size cap does not bound discovery memory usage and a large file can exhaust the server heap. Consider checking the file size via stat (or doing a bounded read) before reading the full contents.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 390:

`discoverCursorSkills` calls `fileSystem.readFileString` to read each `SKILL.md` entirely into memory, and only afterward does `parseCursorSkillMarkdown` enforce the 64 KiB cap. A multi-megabyte `SKILL.md` under a scanned skills root is therefore fully materialized on every invocation before being rejected, so the size cap does not bound discovery memory usage and a large file can exhaust the server heap. Consider checking the file size via `stat` (or doing a bounded read) before reading the full contents.

/** Strip leading YAML frontmatter; returns body only (trimmed). */
export function stripYamlFrontmatter(content: string): string {
const normalized = content.replace(/^\uFEFF/, "");
if (!normalized.startsWith("---")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium provider/cursorSkillDiscovery.ts:79

stripYamlFrontmatter treats any content starting with --- and any later line starting with --- as YAML frontmatter delimiters, without requiring delimiter-only lines. A skill file that begins with a Markdown horizontal rule (---) and has another horizontal rule later silently loses everything through the second rule; strings like ---not-frontmatter are also misparsed as frontmatter. Match complete delimiter lines only (i.e., --- followed immediately by a newline or end-of-string).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 79:

`stripYamlFrontmatter` treats any content starting with `---` and any later line starting with `---` as YAML frontmatter delimiters, without requiring delimiter-only lines. A skill file that begins with a Markdown horizontal rule (`---`) and has another horizontal rule later silently loses everything through the second rule; strings like `---not-frontmatter` are also misparsed as frontmatter. Match complete delimiter lines only (i.e., `---` followed immediately by a newline or end-of-string).

return prompt;
}

const appliedNames = new Set(applied.map((skill) => skill.name.toLowerCase()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium provider/cursorSkillDiscovery.ts:467

applyCursorSkillMentions collapses every run of two or more spaces or tabs across the entire prompt when it removes a $skill mention. Any unrelated whitespace-sensitive content — indented code blocks, tables, aligned text — is destroyed. For example, a prompt containing Python code with 4-space indentation gets those indent levels collapsed to a single space whenever any skill mention is expanded. The .replace(/[ \t]{2,}/g, " ") cleanup on line 472 applies globally instead of only around the removed token, so it rewrites content far from the deleted mention. Consider limiting whitespace normalization to the immediate vicinity of each removed $skill token rather than the whole prompt.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 467:

`applyCursorSkillMentions` collapses every run of two or more spaces or tabs across the entire prompt when it removes a `$skill` mention. Any unrelated whitespace-sensitive content — indented code blocks, tables, aligned text — is destroyed. For example, a prompt containing Python code with 4-space indentation gets those indent levels collapsed to a single space whenever any skill mention is expanded. The `.replace(/[ \t]{2,}/g, " ")` cleanup on line 472 applies globally instead of only around the removed token, so it rewrites content far from the deleted mention. Consider limiting whitespace normalization to the immediate vicinity of each removed `$skill` token rather than the whole prompt.

Comment on lines +424 to +427
function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string {
const body = skillBodyForInjection(skill.content) ?? "";
return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High provider/cursorSkillDiscovery.ts:424

formatInjectedSkillBlock re-runs skillBodyForInjection on skill.content that was already stripped during discovery and again at line 457, so a legitimate skill body starting with --- is parsed as YAML frontmatter and everything up to the next --- line is silently removed before injection. The injected text drops instructions the author wrote. Avoid re-stripping already-normalized content — pass the body through unchanged.

Suggested change
function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string {
const body = skillBodyForInjection(skill.content) ?? "";
return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n");
}
function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string {
const body = skill.content;
return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n");
}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around lines 424-427:

`formatInjectedSkillBlock` re-runs `skillBodyForInjection` on `skill.content` that was already stripped during discovery and again at line 457, so a legitimate skill body starting with `---` is parsed as YAML frontmatter and everything up to the next `---` line is silently removed before injection. The injected text drops instructions the author wrote. Avoid re-stripping already-normalized content — pass the body through unchanged.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

4 blocking correctness issues found. This PR introduces significant new capability (Cursor FS skill discovery and $mention injection) with ~2000 lines of new code. Multiple unresolved review comments exist, including a high-severity bug where skill content can be silently dropped during injection. New features of this scope with identified bugs warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@blaqat

blaqat commented Jul 22, 2026

Copy link
Copy Markdown
Author

Closing — this PR was opened by mistake; not intended for review.

@blaqat blaqat closed this Jul 22, 2026
@blaqat
blaqat deleted the feat/cursor-fs-skill-discovery branch July 22, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant