-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
openspec init --global: Install skills and commands to tool global directories instead of project directories
Problem
openspec init and openspec update always install skills and commands into the current project directory (e.g. .claude/skills/, .cursor/commands/). This works well when you want to commit OpenSpec tooling into a repo you own, but it creates a real problem for consultants, agencies, and developers who work across many client repositories they don't control.
The current behavior forces a choice between:
- Committing OpenSpec files into a client repo (often not acceptable)
- Manually re-running
openspec initand maintaining skills in every project (high friction, no central update path)
This is already partially solved for schemas — ~/.local/share/openspec/schemas/ is a documented global path that works across all projects without touching any repo. Skills and commands need the same treatment.
Several supported tools already have first-class global installation paths:
| Tool | Global path |
|---|---|
| Claude Code | ~/.claude/skills/ and ~/.claude/commands/ |
| OpenCode | ~/.config/opencode/skills/ and ~/.config/opencode/commands/ |
| Codex | ~/.codex/prompts/ (already implemented this way today) |
Codex is a proof of concept that this already works inside OpenSpec's own codebase — its configurator returns an absolute path rather than a project-relative one, which is exactly the pattern this feature extends to the other tools.
Proposed Implementation
The fix is a targeted extension to the existing configurator interface, not a new subsystem.
Each tool configurator currently implements two abstract methods on SlashCommandConfigurator:
abstract getRelativePath(commandId: string): string // project-local path
abstract getFrontmatter(commandId: string): stringThe proposal is to add a third:
abstract getGlobalPath(commandId: string): string | null
// Returns an absolute path if the tool has a known global directory
// Returns null if the tool has no global filesystem path (Cursor, Windsurf, etc.)The base class generateAll() already handles file writing. It would route to getGlobalPath() instead of getRelativePath() when --global is passed, and skip/warn for configurators that return null. The existing marker-based updateExisting() logic applies unchanged — if the file exists at the global path with OpenSpec markers, it gets updated; if not, it was never globally installed. No separate manifest or tracking file is needed.
Example implementations:
// claude.ts
getGlobalPath(commandId: string): string | null {
const base = process.platform === 'win32'
? path.join(process.env.APPDATA!, 'Claude')
: path.join(os.homedir(), '.claude')
return path.join(base, 'skills', `openspec-${commandId}`, 'SKILL.md')
}
// opencode.ts
getGlobalPath(commandId: string): string | null {
const base = process.env.XDG_CONFIG_HOME
? path.join(process.env.XDG_CONFIG_HOME, 'opencode')
: path.join(os.homedir(), '.config', 'opencode')
return path.join(base, 'skills', `openspec-${commandId}`, 'SKILL.md')
}
// cursor.ts
getGlobalPath(commandId: string): string | null {
return null // Cursor has no global filesystem path for commands
}The Codex configurator already returns an absolute path from getRelativePath(). It should be migrated to getGlobalPath() as part of this change, with getRelativePath() restored to return a project-local path. This makes the intent explicit and consistent across all configurators.
CLI surface
# Install globally — --tools required to avoid surprising writes to home dirs
openspec init --global --tools claude,opencode
# Install globally for all tools that have a known global path
openspec init --global --tools all
# Preview what would be written without touching anything
openspec init --global --tools claude --dry-run
# Update all globally-installed skills to latest templates
openspec update --global
# Remove globally-installed files
openspec uninstall --global --tools claude
openspec uninstall --global --tools allAn env var OPENSPEC_GLOBAL_ROOT overrides the base path for all global installs, useful for testing and enterprise setups where home directories are managed centrally.
Global path reference by tool and OS
| Tool | macOS | Linux | Windows | Notes |
|---|---|---|---|---|
| Claude Code | ~/.claude/ |
~/.claude/ |
%APPDATA%\Claude\ |
Skills → skills/openspec-*/, commands → commands/openspec/ |
| OpenCode | ~/.config/opencode/ |
~/.config/opencode/ |
%APPDATA%\opencode\ |
Respects $XDG_CONFIG_HOME |
| Codex | ~/.codex/prompts/ |
~/.codex/prompts/ |
%USERPROFILE%\.codex\prompts\ |
Already global — migrate to getGlobalPath() |
| Cursor | ❌ | ❌ | ❌ | Global rules are UI-only in Cursor settings |
| Windsurf | ❌ | ❌ | ❌ | Project-local only |
| Others | ❌ unknown | ❌ unknown | ❌ unknown | Return null; skip with warning |
macOS note: Claude Code and OpenCode use
~/.claude/and~/.config/opencode/rather than~/Library/Application Support/. OpenSpec should follow each tool's own documented convention.Windows note: Windows paths should be confirmed against each tool's actual behaviour before implementation. Prefer "unknown / not yet supported" over a guess.
Acceptance Criteria
Configurator interface
-
SlashCommandConfiguratorbase class gains an abstractgetGlobalPath(commandId): string | nullmethod - All existing configurators implement
getGlobalPath()— returning an absolute path ornull - The Codex configurator is migrated so
getRelativePath()returns a project-local path andgetGlobalPath()returns the existing absolute path — behaviour unchanged, intent made explicit - The base class
generateAll()accepts a{ global: boolean }option and routes togetGlobalPath()vsgetRelativePath()accordingly - Configurators returning
nullfromgetGlobalPath()are skipped when--globalis used, with a per-tool warning printed to stderr
CLI behaviour
-
openspec init --global --tools claudewrites skills and commands to Claude Code's global directory — not to CWD -
openspec init --global --tools opencodewrites to OpenCode's global directory -
openspec init --globalwithout--toolsexits with a non-zero error:"--tools is required with --global. Use --tools all to install for all tools with a known global path." -
openspec init --global --tools cursor(tool with no global path) exits with a non-zero error and a clear message -
openspec init --global --tools allinstalls for all tools wheregetGlobalPath()returns non-null, and prints a summary of installed vs skipped -
openspec init --global --dry-runprints what would be written without creating any files -
openspec update --globalregenerates globally-installed files using the existing marker-based update logic -
openspec update(without--global) does not modify globally-installed files -
openspec uninstall --global --tools <id>removes OpenSpec-owned files from the tool's global directory -
OPENSPEC_GLOBAL_ROOTenv var overrides the base path used in allgetGlobalPath()implementations
Compatibility
- Global and project-local installations coexist — project-local takes precedence, consistent with Claude Code and OpenCode's own resolution order
- All existing project-local behaviour is unchanged
- Codex continues to install globally — behaviour unchanged, implementation cleaned up
Discoverability
-
openspec init --helpdocuments--global, notes that--toolsis required, and lists which tools support global installation -
openspec schema which --all(or equivalent) indicates whether each tool's skills are installed globally, locally, or both
Why this approach
The configurator registry is already the source of truth for what gets installed where. Adding getGlobalPath() keeps global path knowledge co-located with each tool's existing path definitions rather than in a separate manifest or lookup table. The Codex configurator demonstrates the pattern already works — this change makes it explicit and extends it consistently across the tools that support it.