Skip to content

Prevent capa install from escaping shadow workspaces via symlinks - #175

Open
Minitour wants to merge 3 commits into
developfrom
fix/shadow-workspace-install-guard
Open

Prevent capa install from escaping shadow workspaces via symlinks#175
Minitour wants to merge 3 commits into
developfrom
fix/shadow-workspace-install-guard

Conversation

@Minitour

Copy link
Copy Markdown
Member

Summary

  • Refuse capa install writes when provider paths collapse through symlinks (e.g. .cursor/skillsskills/), with upfront validation and per-write guards across skills, rules, agents, and plugins.
  • On each capa wrap, validate and repair the shadow workspace: remove provider dirs that link into the real project, fix nested install-path symlinks, and sync missing top-level project entries.
  • Expand the built-in wrap agent rule so agents understand shadow vs real layout, that git works normally, and that capa install commands must not run from the shadow workspace.

Test plan

  • bun test src/shared/__tests__/install-path-guard.test.ts
  • bun test src/cli/utils/wrap/__tests__/validate-shadow-workspace.test.ts
  • bun test src/cli/utils/wrap/__tests__/workspace.test.ts
  • Full suite: bun test (1518 pass)
  • Manual: wrap a project whose provider skill dirs symlink to a shared folder; confirm install is refused on the real project and shadow install stays isolated

Made with Cursor

Minitour and others added 2 commits August 12, 2026 18:59
Refuse install writes through symlink-collapsed provider paths, validate and repair wrap shadow layouts on each session, and improve the agent rule for seamless shadow workspace use.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use title/task fields expected by runTasks so tsc --noEmit passes in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Guard shadow workspace installs against symlink escapes

🐞 Bug fix 🧪 Tests ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Block install writes that traverse symlinked provider paths.
• Repair unsafe shadow workspace layouts before reinstallation.
• Clarify wrap workspace behavior and prohibited capa commands for agents.
Diagram

graph TD
  A["capa install"] --> B["Validate provider roots"] --> C{"Safe path?"}
  C -->|"No"| D["Refuse write"]
  C -->|"Yes"| E["Install provider files"]
  F["capa wrap"] --> G["Repair shadow workspace"] --> H["Reinstall shadow files"]
  G --> I["Sync project links"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Preflight validation only
  • ➕ Centralizes validation.
  • ➕ Fails before installation begins.
  • ➖ Does not protect specialized write paths.
  • ➖ Cannot address filesystem changes after preflight.
2. Permit resolved in-project symlink targets
  • ➕ Supports some linked provider layouts.
  • ➖ Breaks shadow workspace isolation.
  • ➖ Makes capa ownership ambiguous.

Recommendation: The layered approach is best: validate all provider roots up front and guard individual write paths for specialized flows and filesystem changes. Rejecting symlink traversal preserves the strict ownership boundary, while wrap repairs its controlled workspace and reinstalls safely.

Files changed (15) +445 / -9

Enhancement (1) +15 / -3
provider-noise-rule.tsExpand shadow workspace agent instructions +15/-3

Expand shadow workspace agent instructions

• Explains linked source versus shadow-only provider paths, normal Git behavior, and prohibited capa commands.

src/cli/utils/wrap/provider-noise-rule.ts

Bug fix (10) +278 / -6
install-one-skill.tsGuard direct skill installation paths +2/-0

Guard direct skill installation paths

• Checks each provider skill destination before replacing or writing skill content, preventing traversal through symlinked provider directories.

src/cli/commands/install-tasks/helpers/install-one-skill.ts

index.tsAdd install-path validation to task pipeline +2/-0

Add install-path validation to task pipeline

• Runs provider install-root validation before install writes begin.

src/cli/commands/install-tasks/index.ts

validate-install-paths.tsAdd provider install-path validation task +13/-0

Add provider install-path validation task

• Introduces a task that validates resolved providers' writable paths against the project root and symlink policy.

src/cli/commands/install-tasks/validate-install-paths.ts

plugin-install.tsGuard plugin skill destinations +2/-0

Guard plugin skill destinations

• Validates plugin skill destinations before deleting or copying managed skill trees.

src/cli/commands/plugin-install.ts

md-io.tsGuard Markdown instruction writes +5/-2

Guard Markdown instruction writes

• Checks instruction-file targets before creating directories or writing provider-managed Markdown files.

src/cli/utils/agents-file/md-io.ts

subagents.tsGuard subagent directory writes +2/-0

Guard subagent directory writes

• Ensures subagent output directories are capa-owned before materializing subagent files.

src/cli/utils/agents-file/subagents.ts

rules-installer.tsGuard rule directories and files +3/-0

Guard rule directories and files

• Validates rule roots and individual rule files before directory creation and writes.

src/cli/utils/rules-installer.ts

validate-shadow-workspace.tsValidate and repair shadow workspace isolation +96/-0

Validate and repair shadow workspace isolation

• Removes provider entries linked to the real project and nested install-path symlinks, restores missing project links, and requests reinstallation when needed.

src/cli/utils/wrap/validate-shadow-workspace.ts

workspace.tsRepair warm wrap workspaces before reuse +22/-4

Repair warm wrap workspaces before reuse

• Validates existing shadow workspaces before reuse or in-place reinstallation and reinstalls after repair.

src/cli/utils/wrap/workspace.ts

install-path-guard.tsAdd shared capa-owned install-path guard +131/-0

Add shared capa-owned install-path guard

• Collects provider-managed writable paths and rejects destinations outside the project or through symlinked and canonical-path-collapsed components.

src/shared/install-path-guard.ts

Tests (4) +152 / -0
provider-noise-rule.test.tsTest expanded wrap agent guidance +2/-0

Test expanded wrap agent guidance

• Verifies generated guidance explains Git behavior and prohibits capa install commands in shadow workspaces.

src/cli/utils/wrap/tests/provider-noise-rule.test.ts

validate-shadow-workspace.test.tsTest shadow workspace validation and repair +73/-0

Test shadow workspace validation and repair

• Covers syncing project entries, removing linked provider paths and nested install symlinks, and retaining materialized provider directories.

src/cli/utils/wrap/tests/validate-shadow-workspace.test.ts

workspace.test.tsPreserve warm workspace reuse coverage +4/-0

Preserve warm workspace reuse coverage

• Materializes expected provider entries so warm reuse remains valid under new repair checks.

src/cli/utils/wrap/tests/workspace.test.ts

install-path-guard.test.tsTest install-path symlink protections +73/-0

Test install-path symlink protections

• Covers safe directories, unsafe parent symlinks, preflight failures, and safe nonexistent destinations.

src/shared/tests/install-path-guard.test.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Subagent symlink write bypass ✓ Resolved 🐞 Bug ⛨ Security
Description
writeSubAgentFile validates only the subagents directory, but then writes the final filePath
without verifying it isn’t a symlink. A pre-existing symlink at the leaf filename can redirect
writeFileSync (and unlinkSync in removal) outside the intended project/shadow provider path.
Code

src/cli/utils/agents-file/subagents.ts[R88-91]

 const { subagents: sa } = provider;
 const agentsDir = join(projectPath, sa.dir);
+  assertCapaOwnedInstallPath(projectPath, agentsDir);
 mkdirSync(agentsDir, { recursive: true });
Evidence
The code only guards agentsDir and then performs file writes/deletes using filePath without
applying the install-path guard at the leaf, allowing a symlinked filename to redirect the
operation.

src/cli/utils/agents-file/subagents.ts[88-106]
src/cli/utils/agents-file/subagents.ts[109-128]
src/shared/install-path-guard.ts[61-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertCapaOwnedInstallPath()` is applied to the subagents directory (`agentsDir`) but not to the final per-subagent file path before `writeFileSync` / `unlinkSync`. This allows a leaf symlink (e.g. `.claude/agents/<id>.md` → `../src/...`) to redirect writes/deletes outside the provider-owned area.
### Issue Context
`assertCapaOwnedInstallPath()` checks for symlinks along the *path it is given*. Guarding only the parent directory does not detect a symlink at the leaf filename.
### Fix Focus Areas
- src/cli/utils/agents-file/subagents.ts[88-106]
- src/cli/utils/agents-file/subagents.ts[109-128]
### What to change
- In `writeSubAgentFile`, after computing `filePath`, call `assertCapaOwnedInstallPath(projectPath, filePath)` immediately before `writeFileSync`.
- In `removeSubAgentFile`, call `assertCapaOwnedInstallPath(projectPath, filePath)` before `unlinkSync`.
- Add a regression test that creates a symlink at the leaf subagent filename pointing outside the provider dir and asserts the operation throws and does not modify the symlink target.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Windows path containment bypass ✓ Resolved 🐞 Bug ⛨ Security
Description
assertCapaOwnedInstallPath treats a destination as in-project unless relative(root, dest) starts
with .. or is empty. On Windows, path.relative() can produce an absolute path for
cross-volume/UNC destinations, which will not start with .., allowing an out-of-project path to
pass this guard.
Code

src/shared/install-path-guard.ts[R72-75]

+	const rel = relative(root, dest);
+	if (rel.startsWith("..") || rel === "") {
+		throw new Error(
+			`Refusing to install outside the project root: ${formatRel(root, dest)}`,
Evidence
The implementation only checks for .. and empty-string cases on the relative() result; there is
no explicit rejection of absolute rel values, which are possible on Windows for cross-volume
paths.

src/shared/install-path-guard.ts[66-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The project-root containment check relies on:
- `rel.startsWith('..')` and
- `rel === ''`
This is insufficient on Windows because `path.relative(root, dest)` may return an *absolute* path when `root` and `dest` are on different volumes (drive letters) or certain UNC cases. An absolute `rel` does not start with `..`, so the guard can be bypassed.
### Issue Context
This function is intended to be a safety boundary for install-time writes. It should conservatively reject any destination that cannot be proven to be under `projectRoot`.
### Fix Focus Areas
- src/shared/install-path-guard.ts[66-78]
### What to change
- Add an explicit rejection when `rel` is absolute (e.g., `if (isAbsolute(rel) || rel.startsWith('..') || rel === '') throw ...`).
- Consider using a stricter containment check (e.g., compare path roots / drive letters via `parse(root).root === parse(dest).root` before computing relative).
- Add a Windows-only (or mocked-path) regression test ensuring cross-volume absolute rel values are rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/cli/utils/agents-file/subagents.ts
Comment thread src/shared/install-path-guard.ts Outdated
Validate leaf subagent paths before write/delete, reject Windows cross-drive and absolute relative paths, and add regression tests.

Co-authored-by: Cursor <cursoragent@cursor.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.

1 participant