🔐 The security rule that survived every fix: remote OpenClaw intelligence can propose powerful actions, but local VS Code remains the authority for local context, file writes, and shell execution.
# 🦞💻 Building OpenClaw Secure for VS Code: From v0.4.0 to v0.4.3
**Date:** September 27, 2026
**Tags:** OpenClaw, VS Code, AI Agents, Coding Agents, Security, Debugging, TypeScript, Homelab
---
## Why I built this
I wanted OpenClaw inside VS Code to feel much closer to the workflows I already like in GitHub Copilot and Codex:
- sessions available directly in the editor;
- synchronized conversation history;
- a model picker;
- slash commands;
- folder and file context;
- `@file`, `@folder`, `@selection`, and Git-aware `@changed`;
- the ability to ask the agent to create or edit local workspace files;
- terminal execution when genuinely needed;
- streaming responses;
- image attachments;
- stop, queue, steer, rename, pin, and archive controls.
But I did **not** want to get those features by handing a remote agent broad authority over my machine or OpenClaw Gateway.
That security constraint shaped the whole project:
```text
Gateway role: operator
Scopes:
operator.read
operator.write
Not granted:
operator.admin
operator.pairing
node-host
```
Local side effects stay local and visible:
```text
Agent proposes file change
↓
VS Code validates path
↓
Diff/review
↓
I approve selected files
↓
Write happens locally
```
And for the shell:
```text
Agent proposes command
↓
VS Code displays:
exact command
exact working directory
reason
risk warning
↓
I explicitly approve
↓
Command runs locally
```
There is deliberately no permanent “always allow shell” switch.
That sounds straightforward. The interesting part was everything we learned once the v0.4.x build hit a **real VS Code instance connected to a real OpenClaw 2026.9.6 Gateway**.
---
# 🚀 What v0.4.0 added
v0.4.0 was the largest feature jump in the extension so far.
## Session-centric coding-agent UX
The sidebar gained:
- live OpenClaw session loading;
- search;
- Recent / Active / Pinned / Archived / All filters;
- rename;
- pin/unpin;
- archive;
- active-run indicators;
- session token usage;
- Stop Run through `chat.abort`.
The goal was to make the extension feel like a persistent coding workspace rather than a one-off chat box.
## Better context
I added:
- Explorer file/folder context;
- `@selection`;
- `@file:<path>`;
- `@folder:<path>`;
- `@changed`;
- `/smart-context <query>`.
Context is bounded and deliberately filtered.
The extension skips or rejects things such as:
- binary files;
- symbolic-link traversal;
- files outside open workspace roots;
- common `.env` variants;
- private keys;
- certificate/private-key containers;
- common credential files;
- oversized context.
`@changed` uses fixed read-only Git command arrays rather than allowing model-provided shell syntax.
## Workspace edits
OpenClaw can propose a structured write block, but it cannot directly mutate arbitrary filesystem paths.
The client validates:
- workspace name;
- relative path;
- traversal;
- symlinks;
- operation type;
- file and batch size;
- current file state.
Then VS Code shows review information and can open a native diff.
I can:
- accept all files;
- accept only selected files;
- reject the batch;
- undo the most recent approved batch if the files have not changed again.
One important stabilization fix also added a **stale-write check**. If I review a diff, manually edit the target file, and only then click Apply, the extension rechecks the file and refuses to overwrite my newer human change.
## Shell execution
This is intentionally powerful and intentionally gated.
The agent can emit a structured shell proposal. Before execution, VS Code shows:
- exact command;
- exact cwd;
- stated rationale;
- unrestricted execution warning.
Then I choose whether it runs.
The shell bridge also:
- has bounded output;
- has a timeout;
- returns stdout/stderr/exit status to OpenClaw;
- redacts credential-like diagnostics;
- requires a fresh approval each time.
## Attachments and streaming
v0.4.0 also added:
- clipboard image paste;
- drag/drop images;
- image file picker;
- per-file and batch size limits;
- decoded-byte validation rather than trusting WebView metadata;
- proper incremental `deltaText` handling.
## OpenClaw 2026.9.6 compatibility
Before release, I also went back to the exact OpenClaw tag instead of relying only on current upstream code.
That caught important protocol details:
- `sessions.subscribe` returns a nested `list` snapshot;
- reconnect should re-subscribe and recover selected-session state;
- `chat.history` can include an `inFlightRun`;
- a reconnecting client should adopt the in-flight `runId` and buffered text;
- queue modes are `steer`, `followup`, `collect`, and `interrupt`;
- attachment and session schemas needed to match 2026.9.6 exactly.
At that point the code looked ready.
CI was green.
Then I installed it.
---
# 💥 Bug #1: v0.4.0 looked alive, but the entire UI was dead
The first v0.4.0 live test was confusing.
The OpenClaw sidebar rendered.
I could see:
- the header;
- status area;
- session picker;
- model picker;
- buttons;
- composer.
But nothing worked.
The Gateway stayed disconnected.
Sessions stayed on:
```text
Loading sessions...
```
And even **local** controls such as Settings, Pair, New, Refresh, and Context did nothing.
That last detail was the clue.
If this were only a Gateway/network problem, local WebView controls should still react.
They did not.
## The actual root cause
The WebView JavaScript is embedded inside a TypeScript template literal.
So there are two parsing layers:
```text
TypeScript source
↓
template literal is evaluated
↓
HTML string is produced
↓
inline JavaScript is extracted by WebView
↓
browser parses JavaScript
```
Two escape-sensitive pieces looked valid in the TypeScript source but changed meaning when the outer string was evaluated.
The final generated browser script was invalid JavaScript.
That meant:
- HTML rendered;
- CSS rendered;
- JavaScript failed before event listeners were registered.
Normal TypeScript CI did not catch it because the TypeScript file itself was valid.
## v0.4.1 fix
The escaping was corrected, but the important improvement was the test.
I added a regression test that:
1. calls the real `getChatWebviewHtml(...)`;
2. extracts the generated inline `<script>`;
3. parses that **final** JavaScript with `new Function(...)`.
That test protects the artifact the WebView actually executes.
### Lesson
> If code generates code, test the generated code.
TypeScript compilation is not enough when another parser will see the result later.
---
# ✅ v0.4.1: Gateway connected and folder context worked
After v0.4.1, the extension came alive.
The Gateway connected.
Sessions loaded.
Buttons worked.
I added a folder to the chat context and asked:
> Can you see the files in the folder?
OpenClaw correctly described the files that had been supplied through VS Code.
That was a major milestone: the context bridge was working end-to-end.
Then another error appeared.
---
# 💥 Bug #2: “Unrecognized queue mode \"and\"”
The UI showed:
```text
Unrecognized queue mode "and". Valid modes: steer, followup, collect, interrupt.
```
At first glance it looked like the extension had sent the wrong `queueMode`.
It had not.
The word **and** came from a file.
One of the selected files discussed the VS Code plugin itself and included prose along the lines of:
```text
/queue and /steer
```
OpenClaw 2026.9.6 supports inline directives.
Its queue parser can find a whitespace-delimited `/queue` token in message text and treat the following token as a queue-mode argument.
So text that was meant to be documentation:
```text
/queue and /steer
```
could become:
```text
queue mode = "and"
```
That is exactly why the Gateway returned the error.
## Why this was more than a parsing bug
This exposed an important security boundary.
A workspace file is **data**.
It can contain arbitrary strings.
That includes strings that happen to look like commands.
Appending a file to a chat message must not accidentally grant that file command authority.
The model-facing prompt already says workspace contents are untrusted data.
But the Gateway command parser was seeing the combined transport message before that conceptual distinction mattered.
---
# 💥 Bug #3: history selector mismatch
The same live session also showed:
```text
history: sessionId requires messageId
```
The session roster includes a `sessionId`, and I had been carrying it into ordinary `chat.history` requests.
OpenClaw 2026.9.6 allows the field in the schema, but the server imposes a stronger rule:
> `sessionId` is only valid when a specific `messageId` is also supplied.
A normal tail-history refresh does not need that selector.
## v0.4.2 history fix
History request construction was centralized.
Normal history now uses:
```text
sessionKey
limit
optional agentId
```
If a future request wants to use `sessionId`, it must also supply `messageId`.
A unit test now enforces that invariant.
---
# ⚠️ v0.4.2: the obvious command-suppression fix crossed the privilege boundary
The first fix for context-as-command looked obvious.
OpenClaw's `chat.send` schema includes:
```text
suppressCommandInterpretation
```
And that is exactly what I wanted.
So v0.4.2 sent:
```text
suppressCommandInterpretation: true
```
for normal/context turns.
The logic was sound.
The authorization assumption was not.
After installing v0.4.2, the real Gateway returned:
```text
system provenance fields require admin scope
```
## Exact upstream behavior
I checked the exact OpenClaw **v2026.9.6** server code.
The Gateway deliberately groups `suppressCommandInterpretation` with trusted system/provenance inputs.
A regular operator client cannot set it unless it has:
```text
operator.admin
```
This was a useful reminder:
> A field can exist in the public request schema and still have stricter authorization rules in the server handler.
## The tempting wrong fix
The easy fix would have been:
```text
Add operator.admin
```
That would likely have made the error disappear immediately.
I rejected that approach.
The whole point of this extension is to get coding-agent ergonomics **without** silently expanding the Gateway trust boundary.
The extension had already proven that local file writes and shell execution can be controlled locally.
It made no sense to add remote admin authority just to prevent source text from being interpreted as a slash directive.
---
# 🔐 v0.4.3: solve the problem without admin
The final solution keeps:
```text
operator.read
operator.write
```
and removes the admin-only suppression field.
Instead, normal/data messages neutralize slash-prefixed tokens before transport.
For example:
```text
/queue
```
becomes:
```text
\/queue
```
The model can still understand what the source text says.
But OpenClaw's inline directive parser no longer sees a whitespace-delimited command token.
The escaping is intentionally narrow.
It does not rewrite:
- URLs;
- normal path separators;
- slashes embedded inside words;
- already escaped slash tokens.
## Explicit commands still work
There is a very important distinction:
```text
Explicit whole-message command typed by me
↓
sent as an OpenClaw command
Normal prose / files / folder context / tool output
↓
treated as data
↓
slash-looking tokens neutralized
```
The extension already parses the composer before sending.
Local commands such as:
- `/queue <message>`;
- `/steer <message>`;
- `/shell <command>`;
- `/context`;
- `/smart-context`;
are handled intentionally.
Other discovered raw OpenClaw slash commands can still be forwarded verbatim when I explicitly invoke them.
---
# 🧪 The final live test
After v0.4.3 was published, I repeated the same workflow that had previously failed:
1. install the VSIX;
2. reload VS Code;
3. connect to the existing OpenClaw Gateway;
4. load sessions;
5. select the test session;
6. attach the same workspace folder;
7. ask OpenClaw whether it can see the files;
8. continue the conversation.
This time it worked.
No dead WebView.
No invalid queue mode.
No history selector warning.
No admin-scope error.
That made v0.4.3 the first v0.4.x release to finish the whole real-world validation loop successfully.
---
# 🧱 Final security model
The extension now has a useful split between **remote intelligence** and **local authority**.
## Gateway
```text
Role:
operator
Scopes:
operator.read
operator.write
```
Not granted:
```text
operator.admin
operator.pairing
node-host
```
## Reading context
The agent only gets workspace data that the extension selects or resolves through its context features.
The client confines and filters it.
## Writing files
The model proposes.
VS Code validates.
I review.
I approve.
The client rechecks the file before writing.
## Running commands
The model proposes.
VS Code shows the exact command and cwd.
I approve every execution.
There is no persistent auto-approval.
That is the architecture I wanted from the beginning.
---
# 🛡️ Other hardening added during the same cycle
The live bugs got most of the attention, but v0.4.0 also gained several less-visible protections.
## Stale file protection
A file is rechecked after review and before apply.
If it changed in between, the proposed write is rejected.
## Symlink confinement
Inline context and workspace writes reject symlink traversal rather than trusting only normalized text paths.
## Better secret filtering
The context policy blocks broader `.env.*` variants and additional credential/key formats.
`@changed` excludes sensitive paths from Git diff generation before the data is sent.
## Attachment validation
The extension does not trust a WebView-provided image size.
It validates canonical base64 and calculates the real decoded byte count.
## Shell diagnostics
Spawn/runtime errors are redacted before they are written to diagnostics.
## Shell result delivery
The code distinguishes:
- the command itself failing;
- the command succeeding locally but its result failing to return to OpenClaw.
That prevents a dangerous situation where a user might retry a command that already executed successfully.
## Per-run proposal de-duplication
Repeated streaming events inside one agent run do not create duplicate local approval prompts.
But the same legitimate command or write proposal in a later run can be approved again.
---
# 🧭 What changed in my testing strategy
This release changed how I think about extension validation.
## Before
```text
TypeScript check
→ unit tests
→ build
→ VSIX packaging
→ done
```
## Now
```text
TypeScript check
→ unit/security tests
→ generated WebView JS parse
→ Gateway ESM runtime import
→ build
→ package VSIX
→ extract packaged VSIX
→ verify Gateway runtime inside package
→ install in real VS Code
→ connect to real Gateway
→ run session/context/write/shell tests
→ release complete
```
The last part matters.
A protocol client can be perfectly type-correct and still be wrong about:
- server authorization;
- field combinations;
- runtime response shapes;
- browser/WebView generation;
- reconnect behavior.
---
# 📋 My future OpenClaw upgrade checklist
The extension is currently pinned to:
```text
@openclaw/gateway-client 2026.9.6
```
Before moving to a newer OpenClaw release, I now want to verify the exact tagged implementation for:
- `sessions.subscribe`;
- `sessions.messages.subscribe`;
- `sessions.create`;
- `sessions.patch`;
- `chat.send`;
- `chat.history`;
- `chat.abort`;
- queue modes;
- command/directive parsing;
- attachment schema;
- in-flight run recovery;
- pairing/setup-code behavior;
- per-field authorization;
- Gateway-client packaging/runtime behavior.
One of the most useful lessons from this project is:
> **Schema-valid does not automatically mean permission-valid.**
---
# 🔄 Release history from this debugging session
```text
v0.4.0
Major Copilot/Codex-style coding UX
↓
Live failure: WebView script did not start
v0.4.1
Generated WebView JS escaping fixed
↓
Live failure: source text triggered /queue parser
Live failure: history sessionId selector mismatch
v0.4.2
History selector fixed
Attempted suppressCommandInterpretation
↓
Live failure: field requires operator.admin
v0.4.3
Removed admin-only field
Added client-side directive neutralization
Kept least privilege
↓
LIVE TEST SUCCESS
```
That is exactly why I prefer small patch releases during real integration testing rather than pretending a large feature build is perfect because CI is green.
---
# 📦 Current release
Repository:
https://github.com/ryanthemanr0x/openclaw-vscode-plugin
Release:
https://github.com/ryanthemanr0x/openclaw-vscode-plugin/releases/tag/v0.4.3
VSIX:
```text
openclaw-secure-local-v0.4.3.vsix
```
Release commit:
```text
26f7a3fc90b94209c599d88800a4c63f1fe4211b
```
SHA-256:
```text
5253f6d11957dedb78435792ff09b1027ca2ceb8c00fd7cd701995fc76127f93
```
---
# ✅ What I consider stable now
At v0.4.3 I have successfully validated the core workflow:
- connect to OpenClaw;
- load sessions;
- load conversation history;
- select a model;
- send normal messages;
- add folder/file context;
- have OpenClaw read supplied files;
- preserve the least-privilege Gateway role.
The broader v0.4.0 features are also covered by CI and targeted regression tests, but any future protocol upgrade should repeat the full live checklist.
---
# 🧠 Biggest lessons
## 1. Rendered UI does not mean running UI
If HTML renders but every control is dead, check whether the WebView JavaScript ever started.
## 2. Data can accidentally become control
Source code and documentation often contain strings that look like commands.
Treat context as data at every layer.
## 3. Do not widen privilege just because it is convenient
The v0.4.2 admin-scope error had a trivial workaround.
The better solution preserved the architecture.
## 4. Exact version source matters
When integrating with a fast-moving project, inspect the exact tagged version you are actually running.
## 5. CI is necessary, not sufficient
Real VS Code + real Gateway testing found bugs that static checking and packaging could not.
## 6. Convert every real failure into a regression rule
The best outcome from a bug is not only a patch.
It is a test, invariant, or architecture rule that makes the same class of failure harder to reintroduce.
---
# 🎯 Where the project goes next
The next phase should be incremental.
I do not want to immediately pile more features on top of the newly validated baseline.
Good future candidates include:
- moving the WebView JavaScript out of the giant HTML template and into a local CSP-protected asset;
- bounding long-lived in-memory proposal de-duplication sets;
- adding more exact OpenClaw protocol fixtures;
- improving session/run recovery if future live tests expose edge cases;
- continuing to close the UX gap with Copilot/Codex without weakening local approval boundaries.
Gateway profiles are intentionally deferred for now.
The v0.4.3 baseline is finally in the state I wanted:
> **OpenClaw can act like a serious coding assistant inside VS Code, but the local editor remains the authority over local files and shell execution.**
That is the boundary I want to keep as the project grows. 🦞🔐💻🚀