Skip to content

openspec init --global: Install skills and commands to tool global directories #752

@askpatrickw

Description

@askpatrickw

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 init and 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): string

The 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 all

An 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

  • SlashCommandConfigurator base class gains an abstract getGlobalPath(commandId): string | null method
  • All existing configurators implement getGlobalPath() — returning an absolute path or null
  • The Codex configurator is migrated so getRelativePath() returns a project-local path and getGlobalPath() returns the existing absolute path — behaviour unchanged, intent made explicit
  • The base class generateAll() accepts a { global: boolean } option and routes to getGlobalPath() vs getRelativePath() accordingly
  • Configurators returning null from getGlobalPath() are skipped when --global is used, with a per-tool warning printed to stderr

CLI behaviour

  • openspec init --global --tools claude writes skills and commands to Claude Code's global directory — not to CWD
  • openspec init --global --tools opencode writes to OpenCode's global directory
  • openspec init --global without --tools exits 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 all installs for all tools where getGlobalPath() returns non-null, and prints a summary of installed vs skipped
  • openspec init --global --dry-run prints what would be written without creating any files
  • openspec update --global regenerates 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_ROOT env var overrides the base path used in all getGlobalPath() 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 --help documents --global, notes that --tools is 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions