-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: sanitize colons in skill/agent names for Windows path compatibility #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c482dd1
fix: sanitize colons in skill/agent names for Windows path compatibility
tmchow 4aef0bc
fix: use resolveCommandPath in OpenCode sync and add collision tests
tmchow 845a915
fix: align converter dedupe sets and manifest paths with sanitized names
tmchow c418f4d
docs: add solution doc for Windows path fix and clarify category guid…
tmchow 109cdfd
fix: add runtime collision guards for sanitized skill/agent names
tmchow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...solutions/integrations/colon-namespaced-names-break-windows-paths-2026-03-26.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| --- | ||
| title: "Colon-namespaced skill names break filesystem paths on Windows" | ||
| date: 2026-03-26 | ||
| category: integration-issues | ||
| module: cli-converter | ||
| problem_type: integration_issue | ||
| component: tooling | ||
| symptoms: | ||
| - "ENOTDIR error when running bun convert on Windows" | ||
| - "mkdir fails with '.config\\opencode\\skills\\ce:brainstorm'" | ||
| - "All target writers (opencode, codex, copilot, etc.) produce colon paths" | ||
| root_cause: config_error | ||
| resolution_type: code_fix | ||
| severity: high | ||
| related_issues: | ||
| - "https://github.com/EveryInc/compound-engineering-plugin/issues/366" | ||
| related_components: | ||
| - targets | ||
| - sync | ||
| - converters | ||
| tags: | ||
| - windows | ||
| - cross-platform | ||
| - path-sanitization | ||
| - skill-names | ||
| - colons | ||
| --- | ||
|
|
||
| # Colon-namespaced skill names break filesystem paths on Windows | ||
|
|
||
| ## Problem | ||
|
|
||
| Skill names containing colons (e.g., `ce:brainstorm`, `ce:plan`) were used directly as directory names in all target writers and sync paths. Colons are illegal in Windows filenames, causing `ENOTDIR` errors during `bun convert` or `bun install`. | ||
|
|
||
| ## Symptoms | ||
|
|
||
| ``` | ||
| { [Error: ENOTDIR: not a directory, mkdir '.config\opencode\skills\ce:brainstorm'] | ||
| code: 'ENOTDIR', | ||
| path: '.config\\opencode\\skills\\ce:brainstorm', | ||
| syscall: 'mkdir', | ||
| errno: -20 } | ||
| ``` | ||
|
|
||
| This affected every target (OpenCode, Codex, Copilot, Gemini, Kiro, Windsurf, Droid, OpenClaw, Pi, Qwen) because all used `skill.name` directly in `path.join()` calls. | ||
|
|
||
| ## What Didn't Work | ||
|
|
||
| Using `/` (forward slash) as the replacement character was initially considered — turning `ce:brainstorm` into nested directories `ce/brainstorm/`. This was rejected because: | ||
|
|
||
| 1. It introduces unnecessary directory nesting for what's fundamentally a character-replacement problem | ||
| 2. The `isValidSkillName` and `validatePathSafe` functions reject `/` and `\`, so sanitized names would fail existing validation | ||
| 3. The source directories already use hyphens (`skills/ce-brainstorm/`), so the output should match | ||
|
|
||
| ## Solution | ||
|
|
||
| Added `sanitizePathName()` in `src/utils/files.ts` that replaces colons with hyphens: | ||
|
|
||
| ```typescript | ||
| export function sanitizePathName(name: string): string { | ||
| return name.replace(/:/g, "-") | ||
| } | ||
| ``` | ||
|
|
||
| Applied across three layers: | ||
|
|
||
| ### Layer 1: Target writers (10 files) | ||
|
|
||
| Every target writer wraps skill/agent names with `sanitizePathName()` when constructing output paths: | ||
|
|
||
| ```typescript | ||
| // Before | ||
| await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name)) | ||
|
|
||
| // After | ||
| await copyDir(skill.sourceDir, path.join(skillsRoot, sanitizePathName(skill.name))) | ||
| ``` | ||
|
|
||
| ### Layer 2: Sync paths (3 files) | ||
|
|
||
| `src/sync/skills.ts`, `src/sync/commands.ts`, and `src/sync/gemini.ts` received the same treatment. Also fixed a pre-existing bug where `syncOpenCodeCommands` used raw `path.join` instead of `resolveCommandPath` for namespaced command names. | ||
|
|
||
| ### Layer 3: Converter dedupe sets and manifests (3 files) | ||
|
|
||
| Sanitizing paths in writers created a secondary bug: converter dedupe logic used unsanitized names, so a pass-through skill `ce:plan` and a generated skill normalizing to `ce-plan` wouldn't detect the collision — both would write to `skills/ce-plan/` on disk. | ||
|
|
||
| Fixed in three converters: | ||
|
|
||
| - **Copilot**: `usedSkillNames.add(sanitizePathName(skill.name))` instead of raw `skill.name` | ||
| - **Windsurf**: Same pattern for agent skill dedupe set | ||
| - **OpenClaw**: Manifest `skills` array now uses sanitized dir names, matching what the writer creates on disk | ||
|
|
||
| ## Why This Works | ||
|
|
||
| The core issue was a mismatch between the logical name domain (colons as namespace separators) and the filesystem domain (colons illegal on Windows). The fix sanitizes at the boundary — names keep colons in data structures and frontmatter, but paths use hyphens. This matches the source directory convention (`skills/ce-brainstorm/` with frontmatter `name: ce:brainstorm`). | ||
|
|
||
| ## Prevention | ||
|
|
||
| ### 1. Collision detection test | ||
|
|
||
| A test in `tests/path-sanitization.test.ts` loads the real compound-engineering plugin and verifies no two skill or agent names collide after sanitization: | ||
|
|
||
| ```typescript | ||
| test("no two skill names collide after sanitization", async () => { | ||
| const plugin = await loadClaudePlugin(pluginRoot) | ||
| const sanitized = plugin.skills.map((skill) => sanitizePathName(skill.name)) | ||
| const unique = new Set(sanitized) | ||
| expect(unique.size).toBe(sanitized.length) | ||
| }) | ||
| ``` | ||
|
|
||
| ### 2. When adding names to filesystem paths | ||
|
|
||
| Always use `sanitizePathName()` when constructing output paths from skill, agent, or component names. Never pass `skill.name` or `agent.name` directly to `path.join()` in target writers or sync files. | ||
|
|
||
| ### 3. When building dedupe sets in converters | ||
|
|
||
| If a converter reserves names for collision detection, the reserved names must be sanitized to match what the writer will produce on disk. Raw names in the set + normalized names from generators = missed collisions. | ||
|
|
||
| ### 4. Inconsistency with `resolveCommandPath` | ||
|
|
||
| Note that `resolveCommandPath` (used for commands) converts colons to nested directories (`ce:plan` -> `ce/plan.md`), while `sanitizePathName` (used for skills/agents) converts to hyphens (`ce:plan` -> `ce-plan`). This is intentional — commands and skills are different surfaces with different resolution patterns. If a new component type is added, decide which pattern fits and document the choice. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.