Windsurf Global Scope Support
After auditing the implementation against `docs/specs/windsurf.md`, two significant changes were made:
Overview
Windsurf Global Scope Support
Post-Implementation Revisions (2026-02-26)
After auditing the implementation against docs/specs/windsurf.md, two significant changes were made:
-
Agents → Skills (not Workflows): Claude agents map to Windsurf Skills (
skills/{name}/SKILL.md), not Workflows. Skills are "complex multi-step tasks with supporting resources" — a better conceptual match for specialized expertise/personas. Workflows are "reusable step-by-step procedures" — a better match for Claude Commands (slash commands). -
Workflows are flat files: Command workflows are written to
global_workflows/{name}.md(global scope) orworkflows/{name}.md(workspace scope). No subdirectories — the spec requires flat files. -
Content transforms updated:
@agent-namereferences are kept as-is (Windsurf skill invocation syntax)./commandreferences produce/{name}(not/commands/{name}).Task agent(args)producesUse the @agent-name skill: args.
Final Component Mapping (per spec)
| Claude Code | Windsurf | Output Path | Invocation |
|---|---|---|---|
Agents (.md) | Skills | skills/{name}/SKILL.md | @skill-name or automatic |
Commands (.md) | Workflows (flat) | global_workflows/{name}.md (global) / workflows/{name}.md (workspace) | /{workflow-name} |
Skills (SKILL.md) | Skills (pass-through) | skills/{name}/SKILL.md | @skill-name |
| MCP servers | mcp_config.json | mcp_config.json | N/A |
| Hooks | Skipped with warning | N/A | N/A |
| CLAUDE.md | Skipped | N/A | N/A |
Files Changed in Revision
src/types/windsurf.ts—agentWorkflows→agentSkills: WindsurfGeneratedSkill[]src/converters/claude-to-windsurf.ts—convertAgentToSkill(), updated content transformssrc/targets/windsurf.ts— Skills written asskills/{name}/SKILL.md, flat workflows- Tests updated to match
Enhancement Summary
Deepened on: 2026-02-25 Research agents used: architecture-strategist, kieran-typescript-reviewer, security-sentinel, code-simplicity-reviewer, pattern-recognition-specialist External research: Windsurf MCP docs, Windsurf tutorial docs
Key Improvements from Deepening
- HTTP/SSE servers should be INCLUDED — Windsurf supports all 3 transport types (stdio, Streamable HTTP, SSE). Original plan incorrectly skipped them.
- File permissions: use
0o600—mcp_config.jsoncontains secrets and must not be world-readable. Add secure write support. - Extract
resolveTargetOutputRootto shared utility — both commands duplicate this; adding scope makes it worse. Extract first. - Bug fix: missing
result[name] = entry— all 5 review agents caught a copy-paste bug in thebuildMcpConfigsample code. hasPotentialSecretsto shared utility — currently in sync.ts, would be duplicated. Extract tosrc/utils/secrets.ts.- Windsurf
mcp_config.jsonis global-only — per Windsurf docs, no per-project MCP config support. Workspace scope writes it for forward-compatibility but emit a warning. - Windsurf supports
${env:VAR}interpolation — consider writing env var references instead of literal values for secrets.
New Considerations Discovered
- Backup files accumulate with secrets and are never cleaned up — cap at 3 backups
- Workspace
mcp_config.jsoncould be committed to git — warn about.gitignore WindsurfMcpServerEntrytype needsserverUrlfield for HTTP/SSE servers- Simplicity reviewer recommends handling scope as windsurf-specific in CLI rather than generic
TargetHandlerfields — but brainstorm explicitly chose "generic with windsurf as first adopter". Decision: keep generic approach per user's brainstorm decision, with JSDoc documenting the relationship betweendefaultScopeandsupportedScopes.
Overview
Add a generic --scope global|workspace flag to the converter CLI with Windsurf as the first adopter. Global scope writes to ~/.codeium/windsurf/, making workflows, skills, and MCP servers available across all projects. This also upgrades MCP handling from a human-readable setup doc (mcp-setup.md) to a proper machine-readable config (mcp_config.json), and removes AGENTS.md generation (the plugin's CLAUDE.md contains development-internal instructions, not user-facing content).
Problem Statement / Motivation
The current Windsurf converter (v0.10.0) writes everything to project-level .windsurf/, requiring re-installation per project. Windsurf supports global paths for skills (~/.codeium/windsurf/skills/) and MCP config (~/.codeium/windsurf/mcp_config.json). Users should install once and get capabilities everywhere.
Additionally, the v0.10.0 MCP output was a markdown setup guide — not an actual integration. Windsurf reads mcp_config.json directly, so we should write to that file.
Breaking Changes from v0.10.0
This is a minor version bump (v0.11.0) with intentional breaking changes to the experimental Windsurf target:
- Default output location changed —
--to windsurfnow defaults to global scope (~/.codeium/windsurf/). Use--scope workspacefor the old behavior. - AGENTS.md no longer generated — old files are left in place (not deleted).
mcp-setup.mdreplaced bymcp_config.json— proper machine-readable integration. Old files left in place.- Env var secrets included with warning — previously redacted, now included (required for the config file to work).
--outputsemantics changed —--outputnow specifies the direct target directory (not a parent where.windsurf/is created).
Proposed Solution
Phase 0: Extract Shared Utilities (prerequisite)
Files: src/utils/resolve-output.ts (new), src/utils/secrets.ts (new)
0a. Extract resolveTargetOutputRoot to shared utility
Both install.ts and convert.ts have near-identical resolveTargetOutputRoot functions that are already diverging (hasExplicitOutput exists in install.ts but not convert.ts). Adding scope would make the duplication worse.
- Create
src/utils/resolve-output.tswith a unified function:
targetName: string
outputRoot: string
codexHome: string
piHome: string
hasExplicitOutput: boolean
scope?: TargetScope
}): string {
const { targetName, outputRoot, codexHome, piHome, hasExplicitOutput, scope } = options
if (targetName === "codex") return codexHome
if (targetName === "pi") return piHome
if (targetName === "droid") return path.join(os.homedir(), ".factory")
if (targetName === "cursor") {
const base = hasExplicitOutput ? outputRoot : process.cwd()
return path.join(base, ".cursor")
}
if (targetName === "gemini") {
const base = hasExplicitOutput ? outputRoot : process.cwd()
return path.join(base, ".gemini")
}
if (targetName === "copilot") {
const base = hasExplicitOutput ? outputRoot : process.cwd()
return path.join(base, ".github")
}
if (targetName === "kiro") {
const base = hasExplicitOutput ? outputRoot : process.cwd()
return path.join(base, ".kiro")
}
if (targetName === "windsurf") {
if (hasExplicitOutput) return outputRoot
if (scope === "global") return path.join(os.homedir(), ".codeium", "windsurf")
return path.join(process.cwd(), ".windsurf")
}
return outputRoot
}
- Update
install.tsto import and callresolveTargetOutputRootfrom shared utility - Update
convert.tsto import and callresolveTargetOutputRootfrom shared utility - Add
hasExplicitOutputtracking toconvert.ts(currently missing)
Research Insights (Phase 0)
Architecture review: Both commands will call the same function with the same signature. This eliminates the divergence and ensures scope resolution has a single source of truth. The --also loop in both commands also uses this function with handler.defaultScope.
Pattern review: This follows the same extraction pattern as resolveTargetHome in src/utils/resolve-home.ts.
0b. Extract hasPotentialSecrets to shared utility
Currently in sync.ts:20-31. The same regex pattern also appears in claude-to-windsurf.ts:223 as redactEnvValue. Extract to avoid a third copy.
- Create
src/utils/secrets.ts:
const SENSITIVE_PATTERN = /key|token|secret|password|credential|api_key/i
servers: Record<string, { env?: Record<string, string> }>,
): boolean {
for (const server of Object.values(servers)) {
if (server.env) {
for (const key of Object.keys(server.env)) {
if (SENSITIVE_PATTERN.test(key)) return true
}
}
}
return false
}
- Update
sync.tsto import from shared utility - Use in new windsurf converter
Phase 1: Types and TargetHandler
Files: src/types/windsurf.ts, src/targets/index.ts
1a. Update WindsurfBundle type
// src/types/windsurf.ts
command?: string
args?: string[]
env?: Record<string, string>
serverUrl?: string
headers?: Record<string, string>
}
mcpServers: Record<string, WindsurfMcpServerEntry>
}
agentWorkflows: WindsurfWorkflow[]
commandWorkflows: WindsurfWorkflow[]
skillDirs: WindsurfSkillDir[]
mcpConfig: WindsurfMcpConfig | null
}
- Remove
agentsMd: string | null - Replace
mcpSetupDoc: string | nullwithmcpConfig: WindsurfMcpConfig | null - Add
WindsurfMcpServerEntry(supports both stdio and HTTP/SSE) andWindsurfMcpConfigtypes
Research Insights (Phase 1a)
Windsurf docs confirm three transport types: stdio (command + args), Streamable HTTP (serverUrl), and SSE (serverUrl or url). The WindsurfMcpServerEntry type must support all three — making command optional and adding serverUrl and headers fields.
TypeScript reviewer: Consider making WindsurfMcpServerEntry a discriminated union if strict typing is desired. However, since this mirrors JSON config structure, a flat type with optional fields is pragmatically simpler.
1b. Add TargetScope to TargetHandler
// src/targets/index.ts
name: string
implemented: boolean
/**
* Default scope when --scope is not provided.
* Only meaningful when supportedScopes is defined.
* Falls back to "workspace" if absent.
*/
defaultScope?: TargetScope
/** Valid scope values. If absent, the --scope flag is rejected for this target. */
supportedScopes?: TargetScope[]
convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
write: (outputRoot: string, bundle: TBundle) => Promise<void>
}
- Add
TargetScopetype export - Add
defaultScope?andsupportedScopes?toTargetHandlerwith JSDoc - Set windsurf target:
defaultScope: "global",supportedScopes: ["global", "workspace"] - No changes to other targets (they have no scope fields, flag is ignored)
Research Insights (Phase 1b)
Simplicity review: Argued this is premature generalization (only 1 of 8 targets uses scopes). Recommended handling scope as windsurf-specific with if (targetName !== "windsurf") guard instead. Decision: keep generic approach per brainstorm decision "Generic with windsurf as first adopter", but add JSDoc documenting the invariant.
TypeScript review: Suggested a ScopeConfig grouped object to prevent defaultScope without supportedScopes. The JSDoc approach is simpler and sufficient for now.
Architecture review: Adding optional fields to TargetHandler follows Open/Closed Principle — existing targets are unaffected. Clean extension.
Phase 2: Converter Changes
Files: src/converters/claude-to-windsurf.ts
2a. Remove AGENTS.md generation
- Remove
buildAgentsMd()function - Remove
agentsMdfrom return value
2b. Replace MCP setup doc with MCP config
- Remove
buildMcpSetupDoc()function - Remove
redactEnvValue()helper - Add
buildMcpConfig()that returnsWindsurfMcpConfig | null - Include all env vars (including secrets) — no redaction
- Use shared
hasPotentialSecrets()fromsrc/utils/secrets.ts - Include both stdio and HTTP/SSE servers (Windsurf supports all transport types)
function buildMcpConfig(
servers?: Record<string, ClaudeMcpServer>,
): WindsurfMcpConfig | null {
if (!servers || Object.keys(servers).length === 0) return null
const result: Record<string, WindsurfMcpServerEntry> = {}
for (const [name, server] of Object.entries(servers)) {
if (server.command) {
// stdio transport
const entry: WindsurfMcpServerEntry = { command: server.command }
if (server.args?.length) entry.args = server.args
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
result[name] = entry
} else if (server.url) {
// HTTP/SSE transport
const entry: WindsurfMcpServerEntry = { serverUrl: server.url }
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
result[name] = entry
} else {
console.warn(`Warning: MCP server "${name}" has no command or URL. Skipping.`)
continue
}
}
if (Object.keys(result).length === 0) return null
// Warn about secrets (don't redact — they're needed for the config to work)
if (hasPotentialSecrets(result)) {
console.warn(
"Warning: MCP servers contain env vars that may include secrets (API keys, tokens).\
" +
" These will be written to mcp_config.json. Review before sharing the config file.",
)
}
return { mcpServers: result }
}
Research Insights (Phase 2)
Windsurf docs (critical correction): Windsurf supports stdio, Streamable HTTP, and SSE transports in mcp_config.json. HTTP/SSE servers use serverUrl (not url). The original plan incorrectly planned to skip HTTP/SSE servers. This is now corrected — all transport types are included.
All 5 review agents flagged: The original code sample was missing result[name] = entry — the entry was built but never stored. Fixed above.
Security review: The warning message should enumerate which specific env var names triggered detection. Enhanced version:
if (hasPotentialSecrets(result)) {
const flagged = Object.entries(result)
.filter(([, s]) => s.env && Object.keys(s.env).some(k => SE