Skip to content

feat: Add user-scope support via --user flag#11

Merged
gricha merged 3 commits intomainfrom
feat/user-scope
Feb 17, 2026
Merged

feat: Add user-scope support via --user flag#11
gricha merged 3 commits intomainfrom
feat/user-scope

Conversation

@gricha
Copy link
Member

@gricha gricha commented Feb 17, 2026

Add a --user flag to all CLI commands (init, install, add, remove, update, sync, list) enabling personal skills and MCP servers managed at ~/.agents/ that follow users across projects.

The core change introduces a ScopeRoot abstraction (src/scope.ts) that encapsulates resolved paths for either project or user scope. MCP and hook writers are refactored from hardcoded project-relative paths to accept resolver callbacks, enabling user-scope MCP configs written to agent-specific absolute paths (~/.claude.json, ~/.cursor/mcp.json, etc.).

Key differences in user scope:

  • Skills live at ~/.agents/skills/ with a single ~/.claude/skills/ symlink
  • MCP configs written to absolute user-level paths per agent
  • Gitignore management skipped (~/.agents/ is not a git repo)
  • Hook management skipped (no user-scope hook paths defined)
  • Adopted orphans use path:skills/ prefix instead of path:.agents/skills/

New files:

  • src/scope.tsScopeRoot interface and resolveScope()
  • src/agents/paths.ts — user-scope MCP target paths, userMcpResolver(), USER_SKILLS_PARENT

Refactored:

  • src/agents/mcp-writer.ts — accepts McpTargetResolver callback
  • src/agents/hook-writer.ts — accepts HookTargetResolver callback
  • All 7 CLI commands — projectRoot: stringscope: ScopeRoot
  • src/cli/index.ts — extracts --user flag before command dispatch

Introduce a `--user` flag across all CLI commands to manage personal
skills and MCP servers at `~/.agents/` that follow users across projects.

Add `ScopeRoot` abstraction that encapsulates all resolved paths for
either project or user scope. Refactor MCP and hook writers to accept
path resolver callbacks instead of hardcoded project paths, enabling
user-scope MCP configs at agent-specific absolute paths (e.g.
`~/.claude.json`, `~/.cursor/mcp.json`).

User scope skips gitignore management, hooks, and uses a single
`~/.claude/skills/` → `~/.agents/skills/` symlink instead of per-agent
project symlinks.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/eYjE8hoooNGp5Pg6UdkqJAGi-Gkfq6hvhbVo0pwN5t0
Copy link

@sentry-warden sentry-warden bot left a comment

Choose a reason for hiding this comment

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

🟠 [5BT-PRY] TOCTOU race condition in symlink migration (src/cli/commands/init.ts:72) (medium confidence)

The migrateDirectory function in symlinks/manager.ts checks if a destination exists with lstat, then renames files. An attacker could exploit the gap between the check and rename to create a symlink at the destination, causing files to be written to an attacker-controlled location.

Identified by Warden via find-bugs

Replace the hardcoded USER_SKILLS_PARENT (~/.claude) with a
per-agent userSkillsParentDirs field on AgentDefinition.

User-scope symlinks are now agent-specific and correct:
- Claude Code: ~/.claude/skills/
- Cursor: ~/.cursor/skills/
- VS Code Copilot: ~/.copilot/skills/ (not ~/.vscode/)
- Codex: no symlink needed (reads ~/.agents/skills/ natively)
- OpenCode: no symlink needed (reads ~/.agents/skills/ natively)

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/TdmzxZ3w6_kgqAhA6clUBmQ4iMu0_3c3MFx_mRarU10
VS Code Copilot, Codex, and OpenCode all read .agents/skills/
natively at both project and user scope. They don't need symlinks.

Make skillsParentDir optional on AgentDefinition — undefined means
the agent reads .agents/skills/ directly. Only Claude Code and
Cursor still need symlinks (.claude/skills/ and .cursor/skills/).

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/2gT86KQBMq67cxlleIbXDtP24fiqjZ43k4UG890AH7E
Copy link

@sentry-warden sentry-warden bot left a comment

Choose a reason for hiding this comment

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

🟠 [YTL-4BZ] Potential path traversal via DOTAGENTS_HOME environment variable (src/agents/types.ts:28) (medium confidence)

The DOTAGENTS_HOME environment variable is used directly without validation in resolveScope(). A malicious environment value could cause the tool to write configs to arbitrary locations. While intended for testing, if exposed in production contexts, this could be exploited.

🟠 [Q9Q-AKF] TOCTOU race condition in symlink handling (src/agents/types.ts:28) (medium confidence)

The ensureSkillsSymlink function has a time-of-check-to-time-of-use (TOCTOU) race condition. Between lstat() checking the path type and the subsequent unlink()/symlink() operations, an attacker with local access could swap the target, potentially causing symlinks to be created pointing to unintended locations.

Identified by Warden via find-bugs

@gricha gricha marked this pull request as ready for review February 17, 2026 06:44
@gricha gricha merged commit 3b8eec5 into main Feb 17, 2026
12 checks passed
@gricha gricha deleted the feat/user-scope branch February 17, 2026 06:44
Copy link

@cursor cursor bot left a comment

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 and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

/** A skill whose source points to its own install location (adopted orphan). */
function isInPlaceSkill(source: string): boolean {
return source.startsWith("path:.agents/skills/");
return source.startsWith("path:.agents/skills/") || source.startsWith("path:skills/");
Copy link

Choose a reason for hiding this comment

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

isInPlaceSkill overly broad match breaks project-scope gitignore

Low Severity

The isInPlaceSkill function now matches path:skills/ sources regardless of scope, but that prefix is only valid for user scope. In project scope, a path:skills/foo source resolves to <project>/skills/foo (copied into .agents/skills/foo), yet isInPlaceSkill returns true, causing the skill to be excluded from the managed gitignore list. The skill ends up tracked by git when it shouldn't be.

Additional Locations (2)

Fix in Cursor Fix in Web

/** A skill whose source points to its own install location (adopted orphan). */
function isInPlaceSkill(source: string): boolean {
return source.startsWith("path:.agents/skills/");
return source.startsWith("path:.agents/skills/") || source.startsWith("path:skills/");
Copy link

Choose a reason for hiding this comment

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

Identical isInPlaceSkill function duplicated across three files

Low Severity

isInPlaceSkill is defined identically in install.ts, sync.ts, and update.ts. This PR extended the condition in all three copies to add the path:skills/ prefix. Having the same logic replicated increases the risk of inconsistent future updates — if the detection heuristic changes, all three sites must be updated in lockstep.

Additional Locations (2)

Fix in Cursor Fix in Web

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

Comments