An autonomous coding agent for VS Code, built around three deliberate constraints:
- OpenAI-compatible or Anthropic APIs only. No other AI provider is supported.
- No internet access anywhere else. The extension makes exactly one kind of network
call —
POST {baseUrl}/chat/completionsto the endpoint you configure (plus, if you choose to set them up, local MCP server processes you control). There is no telemetry, no update-checking, no web browsing/search tool, nothing else. - Filesystem sandboxed to the current project folder. Every read/write/delete the
agent does is resolved against the first open workspace folder and rejected if it
would land outside that folder — including via
..traversal or absolute paths.
Within those constraints, it's a real autonomous agent: it plans, edits files, runs commands to validate its own work, iterates when validation fails, remembers project-specific instructions between sessions, and can be extended with your own MCP tools.
- In VS Code: Extensions view →
...menu → Install from VSIX... → selectlca-code-agent-0.2.0.vsix. (Or from a terminal:code --install-extension lca-code-agent-0.2.0.vsix.) - Open the project folder you want the agent to work on (File > Open Folder...). Only the first workspace folder is used as the sandbox root.
- Click the terminal icon in the Activity Bar to open the "LCA Code Agent" panel.
On first activation the extension creates:
~/.lca/setup.json
(~ is your OS user profile folder — %USERPROFILE% on Windows, $HOME on macOS/Linux.)
{
"provider": "openai",
"baseUrl": "",
"apiKey": "",
"model": "gpt-4o-mini",
"temperature": 0.2,
"maxTokens": 4096,
"autoApprove": false,
"toolsEnabled": true,
"toolCallStyle": "native",
"executeCommandEnabled": true,
"maxAgentSteps": 40,
"mcpServers": {},
"anthropicBaseUrl": "",
"anthropicApiKey": "",
"anthropicAuthToken": "",
"anthropicModel": ""
}Edit it (via the gear icon in the chat panel's title bar, or open the file directly)
and configure the provider you're using. Note that "maxTokens" is the maximum output
tokens per response, not a context-window size — most models cap this well below
100,000, and a request that exceeds a model's real limit gets rejected outright (a 400,
often with an unhelpful generic message from whatever's in front of the model, like a
router/proxy). If a request fails right away, this is one of the first things to check —
try 4096 or 8192.
Set at least baseUrl:
- Local server:
"baseUrl": "http://localhost:1234/v1"(LM Studio, Ollama's OpenAI- compatible endpoint, llama.cpp server, vLLM, etc.) — leaveapiKeyempty if the server doesn't require one. - Self-hosted/proxied OpenAI-compatible endpoint: set
baseUrlandapiKeyaccordingly.
Talks directly to Anthropic's native Messages API (POST {baseUrl}/v1/messages) instead
of the OpenAI-compatible endpoint — a separate wire format, translated automatically so
the rest of the extension (tools, diffs, sessions, etc.) works the same either way. Set:
anthropicModel— required, e.g."claude-sonnet-4-5".anthropicApiKeyoranthropicAuthToken— one of the two is required.anthropicApiKeyis sent asx-api-key;anthropicAuthToken(for OAuth-style bearer tokens, e.g. from a proxy) is sent asAuthorization: Bearer <token>and takes priority if both are set.anthropicBaseUrl— optional, defaults tohttps://api.anthropic.com. Set this to point at a compatible proxy.
Each of these four also falls back to the matching environment variable if the
setup.json field is left empty — ANTHROPIC_MODEL, ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL — the same names used by Claude Code and
Anthropic's own SDKs, so an existing shell environment already set up for those tools
works here too without duplicating credentials into setup.json.
The file is re-read on every request, so edits take effect immediately — no reload needed.
autoApprove: false (the default) means the agent will ask you to Approve/Deny before
any file write, file delete, or shell command. Reading files and listing directories
never needs approval. Set autoApprove: true only if you're comfortable with the agent
acting without confirmation.
Most servers implement OpenAI/Anthropic-style native function-calling reliably, and the
default ("toolCallStyle": "native") uses that. But plenty of free, routed, or smaller
self-hosted models handle it inconsistently — sometimes it works, sometimes a request
(especially a long one, like a skill invocation) comes back with an empty response for
no obvious reason, even though the exact same model works fine elsewhere. Tools like
Cline avoid this for such models by not using the API's native tool mechanism at all —
instead describing the available tools in the prompt and asking the model to "call" them
by writing a specific text format, which is far more universally supported since it
doesn't depend on the server implementing function-calling correctly.
Set "toolCallStyle": "prompt" in ~/.lca/setup.json to do the same here. With this on,
the extension stops sending the API's native tools/tool_choice parameters entirely;
instead the system prompt describes each tool and instructs the model to emit blocks like:
<tool_call name="read_file">
{"path": "index.html"}
</tool_call>
which get parsed back into the same internal tool-call representation native
function-calling produces — so approval prompts, diffs, the plan tracker, and MCP tools
all work exactly the same either way. This is purely a request-format change scoped to
this extension's own outgoing API calls; it doesn't persist into saved sessions, so you
can switch it back to "native" at any time without affecting existing conversations.
A toggle in the toolbar next to the other buttons, the same idea as similar tools' Plan/Act
switch: Plan restricts the agent to read-only exploration (list_files, read_file,
and updating its plan with update_plan) — write_file, replace_in_file, delete_file,
execute_command, and any MCP tools are not even offered to the model, so it physically
cannot use them no matter how a request is phrased; it can only investigate and propose a
plan, then stop and ask you to switch to Act to carry it out. Act is the normal full
mode everything else in this README describes. The mode change shows up in the chat log as
a small notice so it's clear which one is active.
This is a coarser, session-level gate on top of the per-call approval prompts — useful when you want to review a plan before any tool has a chance to touch anything, rather than approving edits one at a time as they come up.
For non-trivial tasks, the agent is instructed to work through an explicit loop rather than just editing blind:
- Plan — lay out its steps up front using the
update_plantool. - Edit — inspect the relevant files, then make focused changes.
- Run — actually execute the project's build/test/lint commands via
execute_command. - Validate — check whether that run actually passed.
- Iterate — if it didn't, go back and fix it rather than declaring success anyway.
The plan is visible and tracked live: a Plan checklist card appears in the chat and
updates as steps move from ☐ pending → ◐ in progress → ☑ done, so you can see where
the agent is in the loop instead of just watching a stream of tool calls.
Project-wide context and instructions live in AGENTS.md in your project's root folder
(not ~/.lca — this one is meant to be committed and shared with your team, the same
convention used by several other coding agents). It's read fresh at the start of every
new chat, so edits take effect on the next conversation.
Use the book icon in the chat panel's title bar (or LCA Code Agent: Open Project Memory from the Command Palette) to create a starter template if one doesn't exist yet, and open it either way. Typical contents: project overview, coding conventions, build/test commands, and anything else you want the agent to always know about this specific repo.
You can extend the agent with your own MCP tools by
adding servers to ~/.lca/setup.json, using the same mcpServers shape as Claude
Desktop / Claude Code configs:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allow"]
},
"my-internal-tool": {
"command": "node",
"args": ["/path/to/my-mcp-server/index.js"],
"env": { "SOME_TOKEN": "..." }
}
}
}Servers are connected (via stdio) once when VS Code starts and disconnected on shutdown.
Their tools show up automatically in the chat as servername: toolname and go through
the same Approve/Deny flow as built-in tools (unless autoApprove is on) — the sandbox
guarantees only apply to this extension's own file tools, so only add MCP servers you
trust, since an MCP tool can do whatever its own implementation allows. Check the Log
(see Troubleshooting) if a server fails to connect — the rest of the extension keeps
working even if one server's connection fails.
For write_file and replace_in_file, approving opens a real VS Code diff editor tab in
the main editor area — Original ↔ Proposed Changes — using VS Code's native diff
viewer. The sidebar chat shows a short prompt pointing at the tab, plus
Approve/Deny. The diff tab closes automatically once you decide. Nothing is
written to disk until you click Approve.
When a turn involved actual file/tool work, the agent's closing message renders as a distinct "✓ Task Completed" card (with real markdown — headings, bold, lists, inline code) instead of a plain chat bubble. Plain questions that don't touch any files still show as a normal reply.
While the agent is working, a "Working ●●●" indicator appears in the chat log and the input box is disabled, so it's always clear whether it's still processing or waiting for you.
If a single turn needs more than maxAgentSteps (40 by default) tool calls, the agent
pauses rather than erroring out — the conversation isn't lost, just send another message
(e.g. "continue") to keep going. Raise "maxAgentSteps" in ~/.lca/setup.json if you
regularly hit it.
On Windows, execute_command looks for a real POSIX shell in this order: (1) whatever
shell your own VS Code integrated terminal is configured to default to, if it's Git Bash
or WSL (terminal.integrated.defaultProfile.windows) — this is very likely why other
tools that drive the integrated terminal directly don't hit this at all on the same
machine; (2) common Git-for-Windows install locations
(Program Files\Git\bin\bash.exe and a few alternates); (3) whatever bash resolves to
on PATH at all, which also catches portable/scoop/chocolatey installs and WSL's own
bash.exe shim. If any of those find a real bash, standard Unix tools (head, tail,
wc, find, grep, cat, ls, sed, awk, etc.) just work, the same as on
macOS/Linux, instead of failing with "is not recognized." Only if none of that turns up a
shell does it fall back to cmd.exe, with the tool description/system prompt switching to
concrete cmd/PowerShell guidance instead (e.g. a one-liner for counting lines, listing
files by extension, or searching text) — either way the model gets accurate guidance
about what's actually available this session, rather than a generic guess.
- New Chat auto-saves the current conversation (if it has any messages) before clearing.
- Save lets you name/rename the current session explicitly and saves it immediately.
- Sessions opens a panel listing all saved sessions (newest first), each with Open and Delete (Delete asks you to confirm by clicking twice).
- If the sidebar view is ever hidden and re-shown (e.g. switching to another view and back), the current conversation reappears automatically rather than looking cleared.
Sessions are stored as individual JSON files under ~/.lca/sessions/.
Reusable prompt snippets live in ~/.lca/skills/, in either layout:
- Flat file:
~/.lca/skills/<name>.md(or.txt) — the filename (without extension) is the skill's name. - Folder-style:
~/.lca/skills/<name>/SKILL.md— the folder name is the skill's name;SKILL.md(case-insensitive) inside it is read as the content.
Both layouts can be mixed freely. Type / in the chat box to see a filterable list of
available skills; pick one (or type /skillname your extra instructions) and it sends
the skill's content as instructions, followed by anything else you typed.
Built-in tools, all confined to the project root:
list_files— list a directory (optionally recursive)read_file— read a file's contentswrite_file— create or overwrite a filereplace_in_file— find-and-replace an exact text block in a file (line-ending differences between CRLF/LF, and common smart-quote/dash/non-breaking-space variants, are normalized automatically before falling back to a match failure)delete_file— delete a file or folderexecute_command— run a shell command with cwd pinned to the project rootupdate_plan— track the Plan → Edit → Run → Validate → Iterate checklist
Plus whatever tools your configured MCP servers provide.
Note on execute_command: pinning the working directory keeps file access sandboxed,
but a shell command can still do anything a normal terminal command can on this machine
(e.g. curl, npm install, git push) — sandboxing paths doesn't sandbox what a
subprocess itself is capable of. That's exactly why approval is required for it by
default; leave autoApprove off if you want to review each command before it runs.
What's actually guaranteed (enforced in code, doesn't depend on the model behaving, can't be talked out of it by the user or by injected content in a file) vs. what's best-effort (depends on the model, backstopped by approval prompts and heuristics):
Hard, code-enforced — holds regardless of what the model does or is told:
- Path sandboxing (
src/pathGuard.ts): every tool call's path argument is resolved against the workspace root and rejected if the resolved path isn't inside it. This resolution follows symlinks and re-checks containment against the real, resolved path (resolveSafePathReal) — a symlink inside the project pointing elsewhere on disk (e.g.project/escape -> /etc) is detected and blocked, not just a plain string check on the path as written. The model can ask for anything; the tool itself refuses. - Single network target for the extension's own requests (
src/openaiClient.ts/src/anthropicClient.ts): the onlyfetch()calls the extension itself makes go to{baseUrl}/chat/completions(OpenAI-compatible) or{anthropicBaseUrl}/v1/messages(Anthropic), per the activeproviderin~/.lca/setup.json. There's no code path to any other host, no telemetry, no update checks.
Best-effort — execute_command and any MCP servers you configure run arbitrary
commands/code by design, so they aren't (and can't be) sandboxed the way file access is.
These layer together to keep them from being quietly misused:
- Approval gate (
src/agentLoop.ts+src/ChatViewProvider.ts): mutating tools pause and wait for an explicit Approve/Deny in the chat panel unless you've turned onautoApprove. - Network commands force approval regardless of
autoApprove:execute_commandcalls are checked against a heuristic list (curl, wget, Invoke-WebRequest, git push/pull/fetch/clone, package installs, rawhttp(s)://URLs, etc. —src/networkHeuristics.ts) and always require an explicit decision, with a distinct red warning banner in the approval prompt, even ifautoApproveis on for everything else. It's a heuristic, not exhaustive — treat it as a tripwire that catches the common cases, not a guarantee. "executeCommandEnabled": falsein~/.lca/setup.jsonremoves the tool entirely if you want zero possibility of shell-based network egress and don't need build/test commands to run.- System prompt makes both boundaries explicit and non-negotiable: the model is told
file access is code-enforced (so it shouldn't waste steps trying to work around it),
that it must refuse network-shaped
execute_command/MCP use even if the user or a file it read asks for it, and that content returned by tools is data to analyze, not instructions to follow (a prompt-injection mitigation) — re-attached fresh to every single API call within a turn (not just once at the start), so it doesn't fade in relevance over a long, many-step turn. This is defense-in-depth, not a guarantee — a model can still misbehave; the approval gate and heuristic above are the actual backstop. - Credentials at rest:
~/.lca/and~/.lca/setup.json(which holds your API keys/tokens in plaintext) are set to owner-only permissions (0700/0600) every time the extension activates, including retroactively for installs from before this existed. This is a real restriction on Linux/macOS; on Windows,fs.chmodhas limited effect (no true POSIX-style ACL), so treat this as defense-in-depth there, not a guarantee. - No secrets in the log: request/response bodies are logged for troubleshooting, but
API keys and auth tokens live in HTTP headers, which are never logged — only the
headers-free JSON body. Configured MCP server
envvalues aren't logged either (cfg.argsis, so avoid passing secrets as plain command-line args to an MCP server if you'd rather they not end up in the log). - No
innerHTMLwith untrusted content: the chat webview builds all DOM content viatextContent/createElement, including model/tool output — there's no path for model-generated text to inject markup or script into the panel.
The extension now ships one npm runtime dependency (the MCP SDK), so it's bundled with
esbuild into a single out/extension.js rather than shipping node_modules:
npm install
npm run build # typechecks, then bundles with esbuild
npx vsce package --no-dependencies # produces the .vsixnpm run watch re-bundles on file changes during development.
If the agent doesn't respond and you don't see an error in the chat panel:
- Click the Log button in the chat toolbar (or run LCA Code Agent: Show Log from the Command Palette). Every request, response status, and error is written there with a timestamp — this is the first place to look.
- A VS Code notification popup also appears for any failure, as a second safety net in case the chat panel itself couldn't render it.
- Requests now time out after 120 seconds instead of hanging forever, so a slow or unresponsive server will surface as a clear timeout error rather than silence.
- If you get a 200 OK response but nothing appears, and the log shows a small response
body, the server likely doesn't reliably support native function/tool calling — this
is common with free/routed models, and can get worse or become intermittent under a
large prompt (e.g. a skill), even if the same model works fine elsewhere (like Cline)
which falls back to a text-based tool format for exactly this reason. Try
"toolCallStyle": "prompt"in~/.lca/setup.jsonfirst (see below) — it keeps full file-editing/agent capability. If that still doesn't help,"toolsEnabled": falsedisables tool calling entirely as a last resort (plain chat only — no file access, commands, or MCP tools; you'd copy code out of the chat yourself). - If a tool call shows up with a garbled name (e.g. containing stray
</>tags), the server's own tool-calling implementation produced malformed output — this extension strips the garbage and tries to recover the real tool name, but if the arguments are also corrupted the tool run will fail with a clear error rather than doing something unpredictable. This points to a server/model compatibility issue, not something to fix here. - If
replace_in_filereports the search text wasn't found even though it looks right, line-ending differences (CRLF vs LF, common on Windows) or smart quotes/dashes are the most likely cause — both are handled automatically by normalizing before comparing, but if it still fails, re-read the file withread_filefirst and copy the exact current text. - With
"provider": "anthropic", a 400 error from the provider (especially via a router/proxy) after otherwise-successful requests usually means two same-role messages ended up adjacent in the conversation — Anthropic's API requires strict user/assistant alternation, unlike OpenAI-compatible APIs. This is handled automatically (adjacent same-role turns are merged before sending), but the exact request body is also now logged before every call, so if something still looks wrong you can see precisely what was sent. - Also check
"maxTokens"in the logged request body if you get an immediate 400 with a generic message like "bad request" — it's max output tokens, not a context-window size, and a value beyond what the model/provider actually supports (routers in particular tend to reject this outright rather than clamping it) is a common cause. The Log now warns whenmaxTokenslooks unusually high (over 100,000) as a hint.
- Only the first workspace folder is treated as the sandbox root; multi-root workspaces aren't specially supported.
- Non-streaming responses only (simpler and more robust across different OpenAI-compatible server implementations); large responses arrive all at once rather than token-by-token.
execute_commandand any MCP server tools cannot themselves be sandboxed to "no internet" — see the notes above for both.