settings.json

The engine’s one configuration file. One shape, read from at most two places, merged per key. This page is the whole file — every key, every default, and every error it can raise.

  1. The two scopes
  2. The whole file
  3. sdd
    1. sdd.style
    2. Where specs live
    3. sdd.source
    4. sdd.checkpoints
    5. sdd.uses — what runs the AI phases
    6. sdd.stages: — one phase, different settings
  4. ai — per-plugin defaults
  5. ai.mcpServers
    1. command is made runnable for you
    2. It is a fourth source, not a replacement
    3. A server signed in once stays signed in
    4. The MCP Servers view
  6. env — the gate on ${env:VAR}
  7. defaultFoldersToIgnoreScanning
  8. Every error the sdd: section can raise

The two scopes

Scope Where Applies to
Workspace <workspace root>/.local-workflows/settings.json — committed, reviewed in PRs everyone on this repository or workspace
Profile ~/.local-workflows/settings.json you, in every workspace

Merged per key, workspace wins. A personal MCP server declared in your profile follows you into every repository; a team server with the same name shadows it. For ai.mcpServers the merge unit is one server: an entry is taken whole, never merged field by field — half a server declaration from each scope is not a server anybody wrote.

Every field has a default and both files are optional. A fresh install writes nothing and works.

A malformed file fails loudly and names itself — the error opens with the file’s full path. Plain JSON: no comments, no trailing commas. The one silent path is absence — a scope that declares nothing is the normal case.


The whole file

{
  "sdd": {
    "style": "kiro",
    "source": "ado",
    "checkpoints": "required",
    "stages": {
      "design": { "model": "claude-opus-5", "thinking": "high" }
    }
  },
  "ai": {
    "ai@1": { "model": "auto" },
    "mcpServers": {
      "ado": {
        "command": "npx",
        "args": ["-y", "@azure-devops/mcp@2.9.0", "contoso",
                 "--authentication", "azcli"],
        "allowedTools": ["wit_work_item", "wit_query"]
      }
    }
  },
  "env": {
    "allowed": ["ADO_PAT"],
    "denied": []
  },
  "defaultFoldersToIgnoreScanning": ["node_modules", "dist"]
}

Four top-level keys, and no others. An unknown one is an error, not tolerance: a misspelled env that silently gates nothing is a security setting that looks applied and is not.

Key What it is
sdd everything spec-driven development reads
ai per-plugin default args, plus MCP servers
env what ${env:VAR} may read
defaultFoldersToIgnoreScanning folders the workspace scan skips when there is no .gitignore

Not in this file: where specs live (fixed — see below), the workspace-root pointer (localWorkflows.workspaceRoot, kept in the .code-workspace — see Workspaces), and anything about a run — params, vars and env belong to the file being run.


sdd

It never reaches an AI. The extension reads it and hands each phase only the values that phase needs. Model choice, style id, and source settings stay in the engine.

Key Values Default
sdd.style kiro, spec-kit, or a style you wrote kiro
sdd.source manual | ado | gh unset — + New Spec asks each time
sdd.checkpoints required | optional | none required
sdd.uses a plugin reference ai@1
sdd.stages.<id> plugin args, for one phase

sdd: is a closed set. Those five keys and no others — a sixth is a hard error naming the valid ones, because a misspelled setting that silently does nothing is worse than one that refuses to load. Plugin args do not go here; they go under ai."ai@1" or sdd.stages.<id>.

sdd.style

The process: its phases, its gates, its prompts, and the spec types in the + New Spec menu. kiro and spec-kit ship; your own works the same way.

Where specs live

.local-workflows/specs under the workspace root — always, and it is not configurable. There is no specRoot setting, in this file or in the editor’s; an earlier build had both, both are gone, and writing sdd.specRoot is now an error naming the five keys that are valid.

In a plain folder that path is the repository’s own .local-workflows/specs. With a .code-workspace open it is the .local-workflows/specs of whichever folder localWorkflows.workspaceRoot names — by default the folder the .code-workspace file sits in. One feature spanning five repositories is one spec, not five.

sdd.source

Where the work item behind a spec comes from: written by hand (manual), Azure DevOps (ado), or GitHub (gh).

This is the one key with no default on purpose. Unset means the file does not say, and + New Spec asks each time — which is the difference between a team that has standardised and one that has not.

sdd.checkpoints

Whether the plan gets “run the tests and stop” barriers between groups of tasks. Read by the tasks prompt when the plan is written; nothing else branches on it.

sdd.uses — what runs the AI phases

Names the plugin, ai@1 by default. Nobody writes it until a second AI plugin exists. The plugin’s args come from ai."<plugin ref>" — the lookup is by the reference sdd.uses names, so a team that points it at a different plugin gets that plugin’s defaults.

sdd.stages: — one phase, different settings

Overrides keyed by phase id — the same ids format.house uses in a style.

{
  "sdd": {
    "stages": {
      "design": { "thinking": "high" }
    }
  },
  "ai": {
    "ai@1": { "model": "auto" }
  }
}

The merge is per key, not per block: design above gets thinking: high and keeps model: auto. Naming one setting never silently drops the others. A stage may also override uses: to run a different plugin entirely.


ai — per-plugin defaults

Every key under ai: that is not mcpServers is a plugin reference, spelled exactly as a task would spell it, holding that plugin’s default args everywhere it runs:

{
  "ai": {
    "ai@1": { "model": "auto" }
  }
}

A workflow file’s own plugins: entry, an SDD stage entry, or a task’s own args: win over it — one key at a time, not one block at a time.

Two defaults are worth knowing:

  • model: auto — the provider picks. A pinned model name goes stale, and a team that has not formed an opinion should not be made to hold one.
  • provider has no default here — ai@1’s own manifest defaults it to ghcp. Writing it here again would leak it to a phase that overrides uses: to a plugin that never declared it.

ai.mcpServers

MCP servers for every ai@1 session, in the standard mcpServers entry shape every MCP README shows — copied unedited, plus three engine keys:

{
  "ai": {
    "mcpServers": {
      "ado": {
        "command": "npx",
        "args": ["-y", "@azure-devops/mcp@2.9.0", "contoso"],
        "env": { "ADO_PAT": "${env:ADO_PAT}" },
        "allowedTools": ["wit_work_item", "wit_query"],
        "deniedTools": ["wit_work_item_write"]
      }
    }
  }
}
Key    
command string the executable for a stdio server. Write it portably — npx, not npx.cmd
args array  
url string the endpoint for an http server
type string local or http. Inferred from command/url when absent
env object environment for the server process. ${env:VAR} is expanded under the gate
headers object  
timeout number  
disabled boolean true skips the server entirely
allowedTools array only these tools. Absent means every tool
deniedTools array never these tools. A name in both lists is denied — deny wins

The last three are the engine’s own and are stripped before the runtime ever sees the server: allowedTools becomes the SDK’s per-server allowlist, deniedTools session-level exclusions under their wire names.

Wire names. An MCP tool’s wire name is <serverKey>-<toolName> — the server declared as ado above surfaces wit_work_item as ado-wit_work_item. allowedTools and deniedTools take the bare name and the engine adds the prefix; a task writing its own availableTools:/excludedTools: must write the full wire name itself, and one that writes the bare name there matches nothing, silently.

command is made runnable for you

On Windows there is no npx — there is npx.cmd, a batch shim, and process creation does not consult PATHEXT. So the declaration stays portable and the engine makes it run: a command with no file extension is resolved through the shell on Windows. A command that already carries one — node.exe, an absolute path — is left exactly as written.

It is a fourth source, not a replacement

The Copilot CLI’s own discovery files still apply:

File Scope
~/.copilot/mcp-config.json every session on this machine
.mcp.json in the workspace this repository — commit it
.github/mcp.json in the workspace same, GitHub’s preferred spot

ai.mcpServers is layered on top of those explicitly. The difference worth caring about is that it is the only one the extension can show you, toggle for you, and merge across scopes.

The editor’s own mcp.json — user-level or .vscode/mcp.json — is not among the CLI’s files and is not bridged across. A server that answers in the chat panel says nothing about a task.

A mcpServers: block in a workflow’s or a task’s args: is not supported. It is deleted before the session is created, from every direction — MCP servers are machine setup, not workflow definition.

A server signed in once stays signed in

A server that authenticates over OAuth has nobody to ask during a run — no window belongs to the task. Tokens are kept where the runtime keeps them rather than discarded with the session, so one interactive sign-in holds for every task after it.

The MCP Servers view

The sidebar’s MCP Servers section is a view over these two files, collapsed by default and hidden entirely until a scope declares a server. Every server both scopes declare shows up, labelled with which file declared it. Ticking a checkbox writes disabled, allowedTools or deniedTools back into that same file, so what the panel shows and what a session resolves can never be two different truths.

Command  
Local Workflows: Start MCP Server (Discover Tools) spawn (or reach) the server, handshake, list its tools, disconnect
Local Workflows: Reload MCP Servers re-read both files
Local Workflows: Open MCP Config open the file that declared the selected server

Start does not leave anything running. The Copilot CLI spawns its own server per session, so a probed server is not a process your tasks talk to — discovery is the honest version of “start”.


env — the gate on ${env:VAR}

${env:VAR} inside any string value of an MCP declaration reads the machine’s environment — but only if something a reviewer can see grants the name. A committed settings file arrives with the repository, so nothing reads your environment by default.

Three grants, one veto:

  Granted by
env.allowed this file — reviewed in the same PR as the servers that use it
the workflow’s own env: block, and .env values the file the task lives in. Its value also wins over the machine’s
allowedEnv: on the ai@1 task the workflow’s author, seen by its reviewer
env.denied always wins. A name written here is unreadable no matter what any repository says

A name nothing grants fails the task with an error saying how to grant it — rather than expanding to an empty string and starting a server that cannot authenticate.

Names only, never values. Nothing here ever holds a secret.


defaultFoldersToIgnoreScanning

Folders the workspace scan skips when the repository has no .gitignore to say so itself. Names, not globs. With a .gitignore, the scan follows that instead and this is not consulted.

Declaring it replaces the built-in list for that scope rather than adding to it. The built-in list is:

node_modules  .git  .vscode  .idea  TestResults
bin  obj  dist  out  coverage  build  target
logs  tmp  temp

Every error the sdd: section can raise

All raised when the file is read, not in the middle of a run:

The file says The error
anything that is not valid JSON '<path>' is not valid JSON: ... Plain JSON only - no comments.
a top-level key that is not sdd, ai, env or defaultFoldersToIgnoreScanning '<path>' has an unknown setting '<key>'. Valid sections: ...
a key under sdd: that is not one of the five settings.json has an unknown key 'sdd.<key>'. Valid: style, source, checkpoints, uses, stages.
sdd.style: or sdd.uses: that is not text settings.json 'sdd.style' must be text.
a sdd.source: or sdd.checkpoints: value not in its list settings.json has 'sdd.source: x' - expected one of 'manual', 'ado', 'gh'.
sdd.stages: that is not a mapping settings.json 'sdd.stages' must be a mapping of stage id to settings.
a sdd.stages.<id> entry that is not a mapping settings.json 'sdd.stages.<id>' must be a mapping.

A key inside a sdd.stages.<id> entry is not checked here — those are plugin args by design, and the plugin validates its own. The JSON schema the extension contributes for settings.json is what flags a genuinely misplaced key while you type.


Back to top

Local Workflows is a VS Code extension. Everything it does is declared in a YAML file you own.


- 22-Aug-2026 08:02 PM +0000