Skip to content

Commit 1c697b9

Browse files
Add a /terminal command that will launch Claude's CLI but using Copilot endpoints (#3235)
This way you can use your Copilot Subscription with the Claude Code CLI.
1 parent 35e9c5b commit 1c697b9

File tree

7 files changed

+426
-6
lines changed

7 files changed

+426
-6
lines changed

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,11 @@
21352135
"title": "Open Memory Files",
21362136
"category": "Claude Agent"
21372137
},
2138+
{
2139+
"command": "copilot.claude.terminal",
2140+
"title": "%github.copilot.command.claude.terminal%",
2141+
"category": "Claude Agent"
2142+
},
21382143
{
21392144
"command": "github.copilot.cli.sessions.delete",
21402145
"title": "%github.copilot.command.deleteAgentSession%",
@@ -5346,6 +5351,10 @@
53465351
{
53475352
"name": "memory",
53485353
"description": "Open memory files (CLAUDE.md) for editing"
5354+
},
5355+
{
5356+
"name": "terminal",
5357+
"description": "Launch Claude Code CLI using your GitHub Copilot subscription"
53495358
}
53505359
]
53515360
},

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@
365365
"github.copilot.config.tools.defaultToolsGrouped": "Group default tools in prompts.",
366366
"github.copilot.config.claudeAgent.enabled": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic's Claude Agent SDK directly in the editor. Uses your existing Copilot subscription.",
367367
"github.copilot.config.claudeAgent.allowDangerouslySkipPermissions": "Allow bypass permissions mode. Recommended only for sandboxes with no internet access.",
368+
"github.copilot.command.claude.terminal": "New CLI Session",
368369
"github.copilot.config.cli.customAgents.enabled": "Enable Custom Agents for Background Agents.",
369370
"github.copilot.config.cli.mcp.enabled": "Enable Model Context Protocol (MCP) server for Background Agents.",
370371
"github.copilot.config.backgroundAgent.enabled": "Enable the Background Agent. When disabled, the Background Agent will not be available in 'Continue In' context menus.",

src/extension/agents/claude/AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ Slash commands often need to present choices or gather input from users. When do
242242
The `show*` APIs are sufficient for most slash command use cases and result in cleaner, more maintainable code. Only use `create*` APIs when you need advanced features like dynamic item updates, multi-step wizards, or custom event handling.
243243

244244
**Current Slash Commands:**
245-
- `/hooks` - Display information about registered hooks (from `hooksCommand.ts`)
246-
- `/memory` - Memory management commands (from `memoryCommand.ts`)
245+
- `/hooks` - Configure Claude Agent hooks for tool execution and events (from `hooksCommand.ts`)
246+
- `/memory` - Open memory files (CLAUDE.md) for editing (from `memoryCommand.ts`)
247+
- `/agents` - Create and manage specialized Claude agents (from `agentsCommand.ts`)
248+
- `/terminal` - Create a terminal with Claude CLI configured to use Copilot Chat endpoints (from `terminalCommand.ts`)
247249

248250
### Tool Permission Handlers
249251

@@ -260,7 +262,7 @@ Tool permission handlers control what actions Claude can take without user confi
260262
- **Common handlers** (`common/toolPermissionHandlers/`):
261263
- `bashToolHandler.ts` - Controls bash/shell command execution
262264
- `exitPlanModeHandler.ts` - Manages plan mode transitions
263-
265+
264266
- **Node handlers** (`node/toolPermissionHandlers/`):
265267
- `editToolHandler.ts` - Handles file edit operations (Edit, Write, MultiEdit)
266268

src/extension/agents/claude/node/claudeLanguageModelServer.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,23 @@ export class ClaudeLanguageModelServer extends Disposable {
120120
}
121121

122122
/**
123-
* Verify nonce
123+
* Verify nonce from x-api-key or Authorization header
124124
*/
125125
private async isAuthTokenValid(req: http.IncomingMessage): Promise<boolean> {
126-
const authHeader = req.headers['x-api-key'];
127-
return authHeader === this.config.nonce;
126+
// Check x-api-key header (used by SDK)
127+
const apiKeyHeader = req.headers['x-api-key'];
128+
if (apiKeyHeader === this.config.nonce) {
129+
return true;
130+
}
131+
132+
// Check Authorization header with Bearer prefix (used by CLI with ANTHROPIC_AUTH_TOKEN)
133+
const authHeader = req.headers['authorization'];
134+
if (typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
135+
const token = authHeader.slice(7); // Remove "Bearer " prefix
136+
return token === this.config.nonce;
137+
}
138+
139+
return false;
128140
}
129141

130142
private async readRequestBody(req: http.IncomingMessage): Promise<string> {

src/extension/agents/claude/vscode-node/slashCommands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import './agentsCommand';
1010
import './hooksCommand';
1111
import './memoryCommand';
12+
import './terminalCommand';
1213

1314
// Future commands can be added here:
1415
// import './settingsCommand';
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { execFile } from 'child_process';
7+
import { promisify } from 'util';
8+
import * as vscode from 'vscode';
9+
import { ILogService } from '../../../../../platform/log/common/logService';
10+
import { ITerminalService } from '../../../../../platform/terminal/common/terminalService';
11+
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
12+
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
13+
import { ClaudeLanguageModelServer } from '../../node/claudeLanguageModelServer';
14+
import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';
15+
16+
const execFileAsync = promisify(execFile);
17+
18+
/**
19+
* Slash command handler for creating a terminal session with Claude CLI configured
20+
* to use Copilot Chat's endpoints.
21+
*
22+
* This command starts a ClaudeLanguageModelServer instance (if not already running)
23+
* and creates a new terminal with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY environment
24+
* variables set to proxy requests through Copilot Chat's chat endpoints.
25+
*
26+
* ## Usage
27+
* 1. In a Claude Agent chat session, type `/terminal`
28+
* 2. A new terminal will be created with the environment variables configured
29+
* 3. Run `claude` in the terminal to start Claude Code
30+
* 4. Claude Code will use Copilot Chat's endpoints for all LLM requests
31+
*
32+
* ## Requirements
33+
* - Claude CLI (`claude`) must be installed and available in PATH
34+
* - The terminal inherits the environment with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY set
35+
* - The language model server runs on localhost with a random available port
36+
*/
37+
export class TerminalSlashCommand implements IClaudeSlashCommandHandler {
38+
readonly commandName = 'terminal';
39+
readonly description = vscode.l10n.t('Launch Claude Code CLI using your GitHub Copilot subscription');
40+
readonly commandId = 'copilot.claude.terminal';
41+
42+
private _langModelServer: ClaudeLanguageModelServer | undefined;
43+
44+
constructor(
45+
@ILogService private readonly logService: ILogService,
46+
@ITerminalService private readonly terminalService: ITerminalService,
47+
@IInstantiationService private readonly instantiationService: IInstantiationService,
48+
) { }
49+
50+
async handle(
51+
_args: string,
52+
stream: vscode.ChatResponseStream | undefined,
53+
_token: CancellationToken
54+
): Promise<vscode.ChatResult> {
55+
stream?.markdown(vscode.l10n.t('Creating Claude Code CLI instance...'));
56+
57+
try {
58+
// Check which CLI is available
59+
const cliCommand = await this._getClaudeCliCommand();
60+
if (!cliCommand) {
61+
const installUrl = 'https://code.claude.com';
62+
const downloadLabel = vscode.l10n.t('Download Claude Code CLI');
63+
if (stream) {
64+
stream.markdown(vscode.l10n.t('Claude Code CLI is not installed. Download Claude Code CLI to get started.'));
65+
stream.button({ command: 'vscode.open', arguments: [vscode.Uri.parse(installUrl)], title: downloadLabel });
66+
} else {
67+
vscode.window.showErrorMessage(
68+
vscode.l10n.t('Claude Code CLI is not installed.'),
69+
downloadLabel
70+
).then(selection => {
71+
if (selection === downloadLabel) {
72+
vscode.env.openExternal(vscode.Uri.parse(installUrl));
73+
}
74+
});
75+
}
76+
return {};
77+
}
78+
79+
// Get or create the language model server
80+
const server = await this._getLanguageModelServer();
81+
const config = server.getConfig();
82+
83+
// Create terminal with environment variables configured
84+
const terminal = this.terminalService.createTerminal({
85+
name: 'Claude',
86+
message: '\n\x1b[1;36m▸ ' + vscode.l10n.t('This instance of Claude Code CLI is configured to use your GitHub Copilot subscription.') + '\x1b[0m\n',
87+
env: {
88+
ANTHROPIC_BASE_URL: `http://localhost:${config.port}`,
89+
ANTHROPIC_AUTH_TOKEN: config.nonce
90+
}
91+
});
92+
93+
// Show the terminal
94+
terminal.show();
95+
96+
// Send the appropriate command to the terminal
97+
terminal.sendText(cliCommand);
98+
99+
this.logService.info(`[TerminalSlashCommand] Created terminal with Claude CLI configured on port ${config.port}, command: ${cliCommand}`);
100+
} catch (error) {
101+
const errorMessage = error instanceof Error ? error.message : String(error);
102+
this.logService.error('[TerminalSlashCommand] Error creating terminal:', error);
103+
if (stream) {
104+
stream.markdown(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
105+
} else {
106+
vscode.window.showErrorMessage(vscode.l10n.t('Error creating terminal: {0}', errorMessage));
107+
}
108+
}
109+
110+
return {};
111+
}
112+
113+
/**
114+
* Check which Claude CLI command is available.
115+
* Returns 'claude' if available, 'agency claude' if agency is available, or undefined if neither.
116+
*/
117+
private async _getClaudeCliCommand(): Promise<string | undefined> {
118+
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
119+
120+
// Check if 'claude' is available
121+
if (await this._isCommandAvailable(whichCommand, 'claude')) {
122+
return 'claude';
123+
}
124+
125+
// Check if 'agency' is available (fallback)
126+
if (await this._isCommandAvailable(whichCommand, 'agency')) {
127+
return 'agency claude';
128+
}
129+
130+
return undefined;
131+
}
132+
133+
/**
134+
* Check if a command is available in PATH
135+
*/
136+
private async _isCommandAvailable(whichCommand: string, command: string): Promise<boolean> {
137+
try {
138+
await execFileAsync(whichCommand, [command]);
139+
return true;
140+
} catch {
141+
return false;
142+
}
143+
}
144+
145+
private async _getLanguageModelServer(): Promise<ClaudeLanguageModelServer> {
146+
if (!this._langModelServer) {
147+
this._langModelServer = this.instantiationService.createInstance(ClaudeLanguageModelServer);
148+
await this._langModelServer.start();
149+
}
150+
151+
return this._langModelServer;
152+
}
153+
}
154+
155+
// Self-register the terminal command
156+
registerClaudeSlashCommand(TerminalSlashCommand);

0 commit comments

Comments
 (0)