Skip to content
| Marketplace
Sign in
Visual Studio Code>Testing>VS Code ControllerNew to Visual Studio Code? Get it now.
VS Code Controller

VS Code Controller

Waldek Mastykarz

|
1 install
| (0) | Free
Drive VS Code and GitHub Copilot Chat from scripts, tests and agents over a local JSON-RPC socket
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

VS Code Controller

VS Code Marketplace Installs License: MIT

Script VS Code and GitHub Copilot Chat like an API. Start a chat, pick the model and agent, send a prompt, wait for the result and keep the edits, all from your own code. No clicking through the UI and no fragile automation of the workbench DOM.

→ {"jsonrpc":"2.0","id":1,"method":"authenticate","params":{"token":"<token>"}}
→ {"jsonrpc":"2.0","id":2,"method":"chat.selectModel","params":{"name":"Example Model"}}
→ {"jsonrpc":"2.0","id":3,"method":"chat.sendPrompt","params":{"prompt":"Add unit tests for src/utils.ts"}}
← {"jsonrpc":"2.0","id":3,"result":{ ... chat result ... }}

Why

You want to run the same prompt against several models, benchmark agents, reproduce a Copilot bug or run Copilot Chat in CI. The usual way is UI automation that clicks through the workbench, and it breaks every time VS Code changes a selector or a layout.

VS Code Controller opens a local JSON-RPC 2.0 socket inside VS Code. Each method maps to a VS Code command, so your scripts use the same stable entry points that the UI uses. Any language that can open a TCP socket can drive it.

Features

  • Full Copilot Chat run from code: start a new chat, choose the harness, model and mode, send a prompt and wait for the response
  • Verified runs: after each prompt, the extension checks that the request really ran with the harness and model you asked for, because VS Code silently falls back when it can't use them
  • Unattended runs: accept tool calls, cancel on timeout, kill terminals, clear notifications and keep all edits
  • MCP control: start or restart MCP servers and wait until their tools are live
  • Any VS Code command: call executeCommand with any command ID and JSON arguments, including URIs
  • Settings: read effective setting values and write workspace settings
  • Many windows, many instances: give each VS Code process its own port and token, and bind the socket to a specific workspace
  • Off by default and authenticated: the socket starts only when you set the environment variables, listens on 127.0.0.1 only, and requires a token
  • Zero-dependency client: a Node.js client library and CLI and a complete prompt runner to start from

Getting started

1. Install the extension

Install VS Code Controller from the Marketplace, or run:

code --install-extension waldek.vscode-controller

Requirements: stable VS Code 1.140 or newer (verified on 1.142), and GitHub Copilot Chat signed in for the chat methods.

2. Launch VS Code with a port and token

The extension does nothing until you set these environment variables on the VS Code process:

export VSCODE_CONTROLLER_PORT=7001
export VSCODE_CONTROLLER_TOKEN="$(openssl rand -hex 32)"
VSCODE_CONTROLLER_WORKSPACE=/path/to/workspace \
"/Applications/Visual Studio Code.app/Contents/MacOS/Code" /path/to/workspace &

3. Send commands

Clone this repository for the example client. In the same shell, check the connection and run a prompt:

git clone https://github.com/waldekmastykarz/vsc-vscodecontroller.git && cd vsc-vscodecontroller
node examples/client.mjs getStatus
node examples/run-prompt.mjs "Add a README" --model "Example Model" --mode agent --timeout 600000

Or connect from any language: open a TCP connection to 127.0.0.1:7001, send one JSON-RPC message per line, and call authenticate first.

Configuration

The client that launches VS Code chooses the values, so it already knows which port and token belong to which instance.

Variable Required Purpose
VSCODE_CONTROLLER_PORT Yes TCP port to listen on. The socket binds to 127.0.0.1 only.
VSCODE_CONTROLLER_TOKEN Yes Secret that clients must send in authenticate before any other call. Use a long random value. Without it, the socket doesn't start.
VSCODE_CONTROLLER_WORKSPACE No Folder or .code-workspace path. Only the window that has it open listens. Use it whenever the VS Code process can have more than one window, for example windows restored from the previous session or opened later. Those windows inherit the same environment variables and also try to open the port. Without it, whichever window starts first gets the socket, which may not be the workspace you launched. The others log EADDRINUSE.

If VS Code is already running with the same user data directory, the launch is forwarded to the existing process. The new window gets that process's environment variables, not the ones you launched with. Close VS Code first or use a separate --user-data-dir.

Diagnostics appear in the VS Code Controller output channel.

Security

Anyone who has the token can do anything that VS Code can do: run any command, edit files, change settings, open terminals and run Copilot agents with your credentials. Treat the token like a password.

The extension limits who can connect:

  • Off by default. Without VSCODE_CONTROLLER_PORT and VSCODE_CONTROLLER_TOKEN, the extension doesn't open a socket.
  • Local only. The socket binds to 127.0.0.1, so other machines on your network can't reach it.
  • Token required. Every connection must call authenticate before any other method. The token is compared in constant time, and a wrong token closes the connection.
  • Message size limit. Messages over 10 MB are rejected and close the connection.

What you should do:

  • Generate a long random token for each launch, for example openssl rand -hex 32, and don't commit it or write it to logs.
  • Don't forward or expose the port, for example with SSH tunnels, VS Code port forwarding, containers that publish ports or a reverse proxy.
  • Remember that every process running as your user can connect to 127.0.0.1 and can usually read VS Code's environment variables. Use the controller on machines and accounts that you trust, such as your own workstation, a CI runner or a disposable VM.
  • When you don't need the controller, launch VS Code without the environment variables or disable the extension.

To report a security issue, open an issue without exploit details, and ask for a private channel.

Protocol

Send one JSON-RPC message per line, terminated by \n. Responses arrive the same way. The server handles requests on a connection concurrently, so you can call chat.cancel while chat.sendPrompt is still waiting. Batches run in order, and notifications (messages without id) get no response.

→ {"jsonrpc":"2.0","id":1,"method":"authenticate","params":{"token":"<token>"}}
← {"jsonrpc":"2.0","id":1,"result":{"authenticated":true}}
→ {"jsonrpc":"2.0","id":2,"method":"chat.selectModel","params":{"name":"Example Model"}}
← {"jsonrpc":"2.0","id":2,"result":{"selector":{"id":"example-model","vendor":"agent-host-copilotcli"},"harness":null}}

The extension opens the socket only after VS Code finishes starting up, so a successful connection means the workbench is ready. Retry the connection until it succeeds.

Error codes

Code Meaning
-32700 Parse error
-32600 Invalid request, or a message over 10 MB, which also closes the connection
-32601 Method not found
-32602 Invalid params. Unknown parameters are rejected.
-32000 The VS Code command failed; message contains VS Code's error
-32001 Not authenticated, or invalid token. An invalid token also closes the connection.

Methods

All parameters are named (an object).

Method Params VS Code command Returns
authenticate token { authenticated: true }
getStatus VS Code version, workspace folders, Copilot Chat state
executeCommand command, args? (array) any The command's result
getSetting key Effective, default, global and workspace values
setSetting key, value? Writes to workspace settings. Omit value to remove the setting.
chat.open workbench.action.chat.open null
mcp.startServer serverId? (default * = all), waitForLiveTools?, timeoutMs? (default 30000) workbench.mcp.startServer null
mcp.restartServer serverId, timeoutMs? (default 30000) workbench.mcp.restartServer null
chat.newChat harness? workbench.action.chat.newChat or workbench.action.chat.openNewChatSessionInPlace.<harness> { harness }
chat.listModels id?, vendor?, family?, version? vscode.lm.selectChatModels Models with id, vendor, family, name
chat.selectModel name? or id?, vendor?, family?, version? workbench.action.chat.open with modelSelector { selector, harness }
chat.setMode mode workbench.action.chat.open with mode null
chat.sendPrompt prompt, waitForResponse? (default true) workbench.action.chat.open with query and blockOnResponse Chat result, or null when not waiting. Fails if the request didn't run in the requested harness or with the selected model.
chat.acceptTool workbench.action.chat.acceptTool null
terminal.kill workbench.action.terminal.kill null
chat.cancel workbench.action.chat.cancel null
notifications.clearAll notifications.clearAll null
chat.keepAllEdits chatEditing.acceptAllFiles null

Harnesses and model selection

A chat runs in a harness (session type), such as local or agent-host-copilotcli (Copilot CLI). Each harness's agent only accepts models from its own vendor. If you select a model from another vendor, VS Code ignores it without an error and uses the agent's default model.

  • chat.newChat without harness lets VS Code pick the session type. Pass harness to choose it explicitly. Calling it with an unknown harness returns the available ones in error.data.available.
  • VS Code can silently replace the requested harness. When your GitHub account's Copilot managed settings contain sandbox.enabled or any permissions.* key, every new chat uses agent-host-copilotcli, even an explicit local one. An administrator can lift this only with the chat.temporaryDefaultToLocalHarness policy. Without such policy, the user settings chat.defaultToCopilotHarness and chat.editor.preferCopilotHarness and the session type you last picked decide the default harness.
  • chat.selectModel with name matches the display name in the model picker and resolves it to a model ID. Without vendor, the extension uses copilot for local, and the harness name for other harnesses. For a chat started without harness, it uses agent-host-copilotcli when that harness is available, and copilot otherwise. Pass vendor to override this.
  • Models load asynchronously after startup. If chat.selectModel fails with No language model named… or No language models found matching selector…, retry after a short delay.

Request verification

VS Code doesn't report when it ignores the requested harness or model, and it doesn't expose either one before a request runs. So after chat.sendPrompt finishes, the extension exports the chat and checks the request:

  • Harness: the agent that handled the request must belong to the harness passed to the last chat.newChat.
  • Model: the model recorded on the request, the agent, and the model the agent reports in the response details must match the last chat.selectModel.

On a mismatch, chat.sendPrompt fails with -32000, and error.data contains expected, served, and the chat result. The prompt has already run when the check fails. The check is skipped when waitForResponse is false. The harness check is also skipped when chat.newChat was called without harness, and the model check when the model was selected without an id or name.

Waiting for responses

chat.sendPrompt resolves when the response completes, fails, or needs user input. In the last case, the result has type: "confirmation" and a kind: toolInvocation, toolPostApproval, confirmation, questionCarousel or elicitation. VS Code has no command to resume waiting after you answer a confirmation. To keep runs unattended, configure auto-approval, for example the user settings chat.tools.global.autoApprove and chat.agent.maxRequests, or chat.permissions.default.

To enforce a timeout, call chat.cancel from the client; the pending chat.sendPrompt then resolves.

Examples

examples/client.mjs is a zero-dependency client library and CLI. Both examples read VSCODE_CONTROLLER_PORT and VSCODE_CONTROLLER_TOKEN.

node examples/client.mjs getStatus
node examples/client.mjs chat.selectModel '{"name":"Example Model"}'

examples/run-prompt.mjs runs a complete prompt. It waits for readiness, opens a new chat, selects the model and agent with retries, sends the prompt, cancels on timeout, then clears notifications and keeps edits.

node examples/run-prompt.mjs "Add a README" --model "Example Model" --mode agent --timeout 600000

Passing URIs to executeCommand

Arguments are plain JSON. For commands that expect a URI, pass a marshalled URI, which VS Code revives automatically:

{"command":"workbench.action.chat.export","args":[{"$mid":1,"scheme":"file","path":"/path/to/chat.json"}]}

Notes

  • mcp.startServer and mcp.restartServer wait until the servers have started. A server that never answers initialize would block them forever, so they fail after timeoutMs. VS Code can't cancel a server start, so the server keeps starting in the background.
  • VS Code has no command to enable or disable a single extension; workbench.extensions.action.disableAll also disables this controller. Change extension enablement before launching VS Code, for example with --disable-extension <id>.
  • setSetting writes workspace settings only. Settings that VS Code restricts to user settings, such as chat.tools.global.autoApprove, return an error.
  • chat.setMode can't confirm that VS Code applied the change. An unknown mode is ignored.

Development

npm install
npm test           # builds and runs the unit tests
npm run package    # builds vscode-controller-<version>.vsix

The Run Extension launch configuration starts an Extension Development Host with the socket enabled on port 7001 and token dev-token. To run from source without the debugger, launch VS Code with --extensionDevelopmentPath=/path/to/this/folder.

License

MIT

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft