---
title: "Windsurf Global Scope Support"
description: "After auditing the implementation against `docs/specs/windsurf.md`, two significant changes were made:"
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/2026-02-25-feat-windsurf-global-scope-support-plan
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T10:58:56.963Z
license: CC-BY-4.0
attribution: "Windsurf Global Scope Support — Claudary (https://claudary.paisolsolutions.com/skills/2026-02-25-feat-windsurf-global-scope-support-plan)"
---

# Windsurf Global Scope Support
After auditing the implementation against `docs/specs/windsurf.md`, two significant changes were made:

## Overview

---
title: Windsurf Global Scope Support
type: feat
status: completed
date: 2026-02-25
deepened: 2026-02-25
prior: docs/plans/2026-02-23-feat-add-windsurf-target-provider-plan.md (removed — superseded)
---

# Windsurf Global Scope Support

## Post-Implementation Revisions (2026-02-26)

After auditing the implementation against `docs/specs/windsurf.md`, two significant changes were made:

1. **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).

2. **Workflows are flat files**: Command workflows are written to `global_workflows/{name}.md` (global scope) or `workflows/{name}.md` (workspace scope). No subdirectories — the spec requires flat files.

3. **Content transforms updated**: `@agent-name` references are kept as-is (Windsurf skill invocation syntax). `/command` references produce `/{name}` (not `/commands/{name}`). `Task agent(args)` produces `Use 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 transforms
- `src/targets/windsurf.ts` — Skills written as `skills/{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
1. **HTTP/SSE servers should be INCLUDED** — Windsurf supports all 3 transport types (stdio, Streamable HTTP, SSE). Original plan incorrectly skipped them.
2. **File permissions: use `0o600`** — `mcp_config.json` contains secrets and must not be world-readable. Add secure write support.
3. **Extract `resolveTargetOutputRoot` to shared utility** — both commands duplicate this; adding scope makes it worse. Extract first.
4. **Bug fix: missing `result[name] = entry`** — all 5 review agents caught a copy-paste bug in the `buildMcpConfig` sample code.
5. **`hasPotentialSecrets` to shared utility** — currently in sync.ts, would be duplicated. Extract to `src/utils/secrets.ts`.
6. **Windsurf `mcp_config.json` is global-only** — per Windsurf docs, no per-project MCP config support. Workspace scope writes it for forward-compatibility but emit a warning.
7. **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.json` could be committed to git — warn about `.gitignore`
- `WindsurfMcpServerEntry` type needs `serverUrl` field for HTTP/SSE servers
- Simplicity reviewer recommends handling scope as windsurf-specific in CLI rather than generic `TargetHandler` fields — but brainstorm explicitly chose "generic with windsurf as first adopter". **Decision: keep generic approach** per user's brainstorm decision, with JSDoc documenting the relationship between `defaultScope` and `supportedScopes`.

---

## 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:

1. **Default output location changed** — `--to windsurf` now defaults to global scope (`~/.codeium/windsurf/`). Use `--scope workspace` for the old behavior.
2. **AGENTS.md no longer generated** — old files are left in place (not deleted).
3. **`mcp-setup.md` replaced by `mcp_config.json`** — proper machine-readable integration. Old files left in place.
4. **Env var secrets included with warning** — previously redacted, now included (required for the config file to work).
5. **`--output` semantics changed** — `--output` now 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.

- [x] Create `src/utils/resolve-output.ts` with a unified function:

```typescript
import os from "os"
import path from "path"
import type { TargetScope } from "../targets"

export function resolveTargetOutputRoot(options: {
  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
}
```

- [x] Update `install.ts` to import and call `resolveTargetOutputRoot` from shared utility
- [x] Update `convert.ts` to import and call `resolveTargetOutputRoot` from shared utility
- [x] Add `hasExplicitOutput` tracking to `convert.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.

- [x] Create `src/utils/secrets.ts`:

```typescript
const SENSITIVE_PATTERN = /key|token|secret|password|credential|api_key/i

export function hasPotentialSecrets(
  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
}
```

- [x] Update `sync.ts` to import from shared utility
- [x] Use in new windsurf converter

### Phase 1: Types and TargetHandler

**Files:** `src/types/windsurf.ts`, `src/targets/index.ts`

#### 1a. Update WindsurfBundle type

```typescript
// src/types/windsurf.ts
export type WindsurfMcpServerEntry = {
  command?: string
  args?: string[]
  env?: Record<string, string>
  serverUrl?: string
  headers?: Record<string, string>
}

export type WindsurfMcpConfig = {
  mcpServers: Record<string, WindsurfMcpServerEntry>
}

export type WindsurfBundle = {
  agentWorkflows: WindsurfWorkflow[]
  commandWorkflows: WindsurfWorkflow[]
  skillDirs: WindsurfSkillDir[]
  mcpConfig: WindsurfMcpConfig | null
}
```

- [x] Remove `agentsMd: string | null`
- [x] Replace `mcpSetupDoc: string | null` with `mcpConfig: WindsurfMcpConfig | null`
- [x] Add `WindsurfMcpServerEntry` (supports both stdio and HTTP/SSE) and `WindsurfMcpConfig` types

### 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

```typescript
// src/targets/index.ts
export type TargetScope = "global" | "workspace"

export type TargetHandler<TBundle = unknown> = {
  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>
}
```

- [x] Add `TargetScope` type export
- [x] Add `defaultScope?` and `supportedScopes?` to `TargetHandler` with JSDoc
- [x] Set windsurf target: `defaultScope: "global"`, `supportedScopes: ["global", "workspace"]`
- [x] 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

- [x] Remove `buildAgentsMd()` function
- [x] Remove `agentsMd` from return value

#### 2b. Replace MCP setup doc with MCP config

- [x] Remove `buildMcpSetupDoc()` function
- [x] Remove `redactEnvValue()` helper
- [x] Add `buildMcpConfig()` that returns `WindsurfMcpConfig | null`
- [x] Include **all** env vars (including secrets) — no redaction
- [x] Use shared `hasPotentialSecrets()` from `src/utils/secrets.ts`
- [x] Include **both** stdio and HTTP/SSE servers (Windsurf supports all transport types)

```typescript
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).\\n" +
      "   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:

```typescript
if (hasPotentialSecrets(result)) {
  const flagged = Object.entries(result)
    .filter(([, s]) => s.env && Object.keys(s.env).some(k => SE

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/2026-02-25-feat-windsurf-global-scope-support-plan) · https://claudary.paisolsolutions.com
