Symlink topology
What this layer does
Phase 2 fans out one canonical copy of every skill to every detected agent runtime on the local machine. One inode, many entrypoints. Edits made through Claude Code are immediately visible to Codex, Gemini, OpenClaw, Cursor, and Windsurf because all six paths resolve to the same file. POSIX close(2) semantics provide last-writer-wins for free.
Files involved
state/bin/sync/symlink-runtimes.sh— idempotent fan-out script. Run
per runtime; safe to re-run.
state/bin/sync/migrate-realdirs.sh— one-time conversion (Phase 11)
for runtimes that already have real ~/.gemini/skills/snappy-* dirs.
runtime-plugins/openclaw/snappy-loader/— bundled OpenClaw plugin,
versioned + synced via DO Spaces.
state/bin/sync-runtimes.ts— generates Codex / Gemini.toml
command files and the canonical 3-file Codex shim (SKILL.md, AGENTS.md, api.ts).
Per-runtime table
| Runtime | Canonical → Target |
|---|---|
| Claude Code | ~/.claude/skills/snappy-<name> → canonical |
| Claude Code commands | ~/.claude/commands/snappy-<name>.md → canonical |
| Codex CLI | ~/.codex/skills/snappy-<name> → canonical (3-file dir) |
| Codex CLI commands | ~/.codex/commands/snappy-<name>.toml (generated) |
| Gemini CLI | ~/.gemini/skills/snappy-<name> → canonical |
| Gemini CLI commands | ~/.gemini/commands/snappy-<name>.toml (generated) |
| OpenClaw | covered by Claude Code symlinks + bundled snappy-loader plugin |
| Cursor | ~/.cursor/skills/snappy-<name> → canonical (when supported) |
| Windsurf | ~/.windsurf/skills/snappy-<name> → canonical (when supported) |
The Codex 3-file shim is generated IN canonical at ~/projects/snappy-os/state/skills/<name>/{SKILL.md,AGENTS.md,api.ts} so the directory shape is uniform across runtimes. The api.ts shim throws on invoke() and points the caller at SKILL.md.
Symlink mechanics
POSIX symlinks resolve at every open(2). No copy, no cache, no drift. Two agents writing the same canonical file race on close(2); the loser is detected by state/bin/sync/conflict-detect.ts (manifest delta watcher) and logged to state/log/sync-conflicts/.
Operational gotchas
- Per skill, never per parent dir. Symlinking
~/.claude/skills/
itself would shadow non-snappy skills. Symlinking each ~/.claude/skills/snappy-<name>/ leaves room for unrelated skills.
- Real-dir collisions get backed up before symlinking. The script moves
any pre-existing real dir to ~/.claude/_backups/<ts>/<runtime>/<name>/ and logs the move to state/log/migrations.ndjson. Never overwrite.
- Cross-drive symlinks fail on Windows. Bootstrap detects shell and
uses mklink /J (junction, no admin needed) on local drive only; cross-drive attempts exit with a clear error.
- Cursor and Windsurf may not expose a skills dir on every install.
The script no-ops gracefully when the parent dir is absent.
- A broken symlink (target gone after manual
rm) is repaired on next
bootstrap idempotency pass; the script checks readlink -f and recreates if missing.
How to verify it's working
readlink ~/.claude/skills/snappy-imageresolves to
~/projects/snappy-os/state/skills/snappy-image.
- Edit
state/skills/snappy-image/SKILL.md, thencatthe path
through every runtime alias — content is identical.
- The C.3 cross-runtime parity matrix passes for every detected runtime.
state/lint/symlink-check.tsexits 0.