# πŸͺ½πŸ”§ Building a Secure Hermes VS Code Coding Agent: From Remote Chat to Local Workspace Writes

**Date:** September 27, 2026  
**Tags:** Hermes, VS Code, AI Agents, Security, Coding Agents, Debugging, Workspace Automation

---

## πŸš€ Why I built this

I wanted Hermes inside VS Code to feel much less like an embedded remote chat window and much more like a real coding-agent experience.

The target was the interaction style I like from modern coding assistants:

- open VS Code
- see connection state immediately
- resume existing sessions
- start a new chat without leaving the editor
- choose the model
- attach files or folders as context
- add a code selection
- use slash commands
- steer or queue a running task
- ask the agent to change the local workspace
- preview what it wants to change
- explicitly approve it
- undo it safely if needed

The difficult part was not the chat UI.

The difficult part was this sentence:

> A remote Hermes instance should be able to help modify my local Windows workspace without giving the remote model unrestricted filesystem access.

That became the design principle for the whole project.

Repository:

https://github.com/ryanthemanr0x/hermes-vscode-plugin

Current release:

https://github.com/ryanthemanr0x/hermes-vscode-plugin/releases/tag/v2.0.1

---

# 🧭 The architecture I settled on

Hermes runs remotely.

VS Code runs locally.

Those two facts matter.

If I attach a local Windows file to Hermes, Hermes can reason about the uploaded snapshot, but the remote Hermes host cannot magically write to a local path such as:

~~~text
Ap3xBeast_Workspace/Projects/Notes/20260926-LinkedIn.md
~~~

That path belongs to the VS Code machine, not the Hermes server.

The architecture therefore became:

~~~text
Local VS Code
   |
   | explicit context upload
   v
Hermes Remote Gateway
   |
   | model reasons about change
   | emits structured proposal
   v
VS Code Workspace Write Bridge
   |
   | parse
   | validate
   | preview
   | approve
   v
Local workspace filesystem
~~~

That separation is the most important security property in the project.

Hermes can propose.

VS Code decides.

---

# πŸ’¬ Phase 1: make Hermes feel native in VS Code

Before file editing, the extension first needed to become a good chat client.

The earlier versions evolved through a series of practical problems.

## Connection status

The sidebar shows whether the extension is connected to the configured Hermes gateway.

That sounds basic, but it immediately answers one of the most frustrating questions during debugging:

> Is the plugin broken, or is the gateway disconnected?

The sidebar now makes that state visible.

## Session loading

The extension can list existing Hermes sessions so I can continue work started elsewhere.

This was important because I did not want VS Code to create an isolated conversation universe.

Hermes Desktop, the web UI, scheduled sessions, and VS Code should all feel like clients of the same backend.

## Transcript hydration bug

One of the earlier problems was that sessions appeared in the list, but opening an older session could result in an empty transcript.

The resume call itself was succeeding.

The problem was data-shape compatibility.

The Hermes gateway had transcript rows where the useful renderer-facing field could be:

~~~text
display_content
text
content
~~~

The client was not checking every representation.

The fix added the correct fallback order and a secondary authenticated messages API fallback when a session reported messages but resume did not hydrate them.

That was an early reminder that remote-agent integrations often fail at the boundary between two individually correct systems.

---

# ⚑ Slash commands, queue, and steer

The next step was making the composer behave more like an agent console.

Instead of hard-coding a list of slash commands in the extension, the client asks Hermes for the live command catalog.

Typing:

~~~text
/
~~~

can surface the commands, skills, bundles, and plugins actually available on the connected Hermes system.

That matters because Hermes changes independently of the VS Code extension.

A hard-coded command list would drift.

The live catalog keeps the client aligned with the server.

I also wanted two important Hermes behaviors to work while a task was already running:

~~~text
/queue <prompt>
/steer <prompt>
~~~

Normally the composer changes to a stop control while an agent turn is active.

The extension now recognizes slash-command drafts and allows those control messages to be sent without cancelling the current run.

That made the interface feel much closer to a real coding-agent console.

---

# πŸ“„ Local context: files, folders, selections, and @file

The next milestone was local context.

Hermes is remote, so local content needs to be shared deliberately.

I did not want the extension silently dumping the whole repository.

The implementation therefore supports several explicit context paths.

## Explorer file context

A file can be added from VS Code Explorer.

The extension:

1. verifies that the file belongs to an open workspace;
2. checks safety rules;
3. enforces a size limit;
4. rejects binary-looking content;
5. uploads a snapshot through Hermes file.attach;
6. stores the returned session-scoped reference;
7. shows the attachment as a removable context chip.

## Folder context

Folders are more dangerous because a single click can accidentally include a dependency tree, repository metadata, build output, or credentials.

Folder snapshots are bounded.

The extension excludes common noisy or sensitive paths such as:

- .git
- node_modules
- build
- dist
- caches
- virtual environments
- selected credential files
- symlinks
- unsupported binary content

It also enforces per-file, total-size, and file-count caps.

The folder snapshot is intentionally read-only context.

## Selected-code context

I also wanted to highlight a small block of code and say:

> Fix this.

The selection path includes:

- workspace name
- relative path
- start line
- end line
- selected text

That gives Hermes useful local structure without uploading the full file.

## @file picker

v2.0 added a composer-oriented file picker.

Typing @ lets me search safe workspace files and attach them directly to the current prompt.

This was one of the UX improvements that moved the extension from β€œremote chat in a panel” toward β€œcoding assistant inside the editor.”

---

# ✍️ The Workspace Write Bridge

This was the biggest architectural change.

The goal was to let Hermes create and edit local files while preserving local control.

The bridge currently understands structured operations such as:

~~~text
create_directory
create_file
edit_file
apply_patch
~~~

Hermes returns those inside a structured control block.

Conceptually:

~~~text
<hermes-workspace-write>
{
  "operations": [
    {
      "type": "create_file",
      "workspace": "Projects",
      "path": "Notes/new-description.md",
      "content": "..."
    }
  ]
}
</hermes-workspace-write>
~~~

The extension does not simply trust that.

The operation goes through several layers first.

---

# πŸ” The security model

This project became much more interesting once local writes were possible.

I did not want:

~~~text
remote model
   ↓
arbitrary local filesystem
~~~

I wanted:

~~~text
remote model proposal
   ↓
deterministic local policy
   ↓
human review / approval
   ↓
bounded local mutation
~~~

The current protections include the following.

## VS Code Workspace Trust

If VS Code considers the workspace untrusted, Hermes local writes are blocked.

That means the extension participates in VS Code's existing trust model rather than inventing a parallel one.

## Relative paths only

Workspace-write paths must be relative.

Absolute paths are rejected.

Windows drive-qualified paths are rejected.

Traversal with .. is rejected.

## Canonical workspace containment

The target is resolved against the real workspace root and checked again for containment.

A valid-looking string is not enough.

The final filesystem target still has to remain inside the approved open workspace.

## Symlink protection

A repository can contain a symlink that visually looks like a normal child path but actually points somewhere else.

The bridge checks the path for symlink traversal rather than relying only on string prefix matching.

## Protected files

The bridge blocks writes to high-risk control and credential locations.

Examples include categories such as:

- repository internals
- SSH material
- cloud credentials
- environment secrets
- token files
- selected VS Code executable configuration

## No silent create overwrite

create_file is intentionally different from edit_file.

If the target already exists, create_file fails instead of replacing it.

That prevents a model from turning a β€œnew file” operation into an accidental overwrite.

## Stale edit protection

For an existing file, the extension remembers the bytes used to create the reviewed proposal.

Immediately before the mutation, it checks again.

If the file changed in the meantime, the edit is rejected.

That closes an important review-to-apply race.

## Approval modes

The extension supports:

~~~text
Writes: Ask
Writes: Session
Writes: Read-only
~~~

Ask is the normal mode.

Session permission is temporary and memory-only.

Read-only shuts off local writes.

## No arbitrary shell

The model-facing bridge still does not expose unrestricted local shell execution.

That is deliberate.

File editing and shell execution have very different risk profiles.

---

# πŸ‘€ Review, diff, rollback, and undo

v2.0 expanded the workflow beyond β€œyes/no.”

For a multi-file proposal, VS Code can show a selectable review list.

I can:

- inspect the proposed files
- deselect changes I do not want
- open native VS Code diffs
- approve the selected operations
- deny everything

The extension also preflights the selected transaction before the first mutation.

If an ordinary failure occurs after some operations have already been applied, it attempts best-effort rollback.

There is also an Undo Last Workspace Change command.

Undo is intentionally conservative.

If I manually edit one of the files after Hermes changed it, undo refuses to overwrite my newer work.

That is exactly how an agent undo should behave.

---

# πŸͺŸ Issue #1: Windows path identity

One of the first local-write failures looked like a workspace escape.

The extension reported a message similar to:

~~~text
Workspace target changed or escaped the approved workspace
~~~

But the requested path was legitimate.

The root cause was Windows path identity.

Node filesystem APIs and vscode.Uri can normalize drive letters and path casing differently.

On Windows, two strings can refer to the same filesystem path even though a literal case-sensitive string comparison says they are different.

The original validation was effectively too strict in the wrong place.

## The fix

The extension added filesystem/platform-aware path equality while keeping the real security checks:

- canonical workspace root
- containment
- traversal rejection
- symlink checks
- protected paths

The lesson was subtle:

> Strong validation does not mean naive string comparison.

Security checks need to match the semantics of the platform they are protecting.

---

# πŸ“ Issue #2: β€œsame folder” did not always mean the same folder

Another problem appeared when I attached a nested file and asked Hermes to create a sibling file.

The human-readable context label did not always preserve enough information for exact local reconstruction.

For example, the visible name might imply β€œNotes,” while the true local path was:

~~~text
Ap3xBeast_Workspace/Projects/Notes
~~~

If Hermes shortened that path, the proposal could target the wrong location.

## The fix

The extension now keeps the original workspace name and full workspace-relative path as structured metadata.

The model-facing bridge instructions explicitly say to preserve the complete context path prefix for sibling or β€œsame folder” requests.

Again, the important idea was:

> Display labels are for humans. Filesystem coordinates are for machines.

Do not confuse them.

---

# 🧠 v2.0: moving toward Copilot/Codex parity

By v2.0, the extension had become much more than a chat window.

The release added:

- @file context
- selected-code context
- line-based apply_patch
- multi-file selective review
- native diffs
- transaction preflight
- mutation-time revalidation
- rollback
- safe undo
- Git dirty-file warnings
- Workspace Trust integration
- session search
- session grouping
- pinning
- rename/archive/delete
- gateway profiles
- connection diagnostics
- Windows and Ubuntu CI validation

At that point, the project finally felt like a real coding workflow.

Then the manual test found one more edge case.

---

# πŸ’₯ Issue #3: valid JSON plus extra JSON closers

The exact test was straightforward.

I attached a Notes file/folder.

Hermes could see it.

Hermes reviewed the current content successfully.

I asked Hermes to create another Markdown file with an improved version.

Hermes produced the content correctly.

But VS Code showed:

~~~text
Invalid Hermes workspace proposal:
Unexpected non-whitespace character after JSON at position 2121
~~~

This was interesting because almost everything had already worked.

The connection was good.

The session was good.

Context upload was good.

Hermes reasoning was good.

The generated file content was good.

The local workspace path was good.

The write permission mode was good.

The failure happened before any of those local write checks mattered.

---

# πŸ”Ž Root cause: the parser was correctly strict

The v2.0 proposal parser did roughly this:

~~~text
extract <hermes-workspace-write>
strip optional JSON fence
JSON.parse(raw)
validate operations
~~~

The actual model output contained one complete valid JSON object followed by extra closing delimiters.

Conceptually:

~~~text
{"operations":[ ... ]} ]}
~~~

The first object was valid.

The trailing ]} was not whitespace.

Native JSON.parse therefore rejected it.

That is exactly what a correct JSON parser should do.

The mistake would have been assuming this was a filesystem bug.

The error position and the rendered model output made it clear that the problem was serialization.

---

# πŸ› οΈ Why I did not want a β€œrepair anything” parser

It would have been easy to write something sloppy like:

> Find the first { and the last }, parse whatever is between them.

That would be a bad idea.

The content field of a workspace operation can contain source code, Markdown, JSON examples, brackets, braces, quotes, and escaped characters.

For example:

~~~text
content = "JSON example: {"items":[1,2]} and code } ]"
~~~

A naive regex or substring repair can misidentify boundaries.

Even worse, a generic best-effort parser at a security boundary starts guessing what an untrusted remote response meant.

I wanted the recovery behavior to be extremely narrow.

---

# βœ… The v2.0.1 parser design

The fix is strict-first.

## Step 1: normal JSON first

The client still starts with native JSON.parse.

If the payload is valid, nothing changes.

That is important.

The compatibility path is not the normal path.

## Step 2: locate one complete top-level object

If strict parsing fails, the helper scans for the end of the first complete top-level JSON object.

The scanner understands:

- object nesting
- array nesting
- quoted strings
- escaped quotes
- backslashes

It is not trying to replace JSON.parse.

It is only determining:

> Where does one complete top-level object end?

## Step 3: inspect the tail

Recovery is allowed only if everything after that object consists of:

- whitespace
- duplicate closing ]
- duplicate closing }

Nothing else.

That means this can recover:

~~~text
VALID_OBJECT + "]}"
VALID_OBJECT + "  } ]  "
~~~

but still rejects:

~~~text
VALID_OBJECT + "I changed another file too"
VALID_OBJECT + "/* comment */"
VALID_OBJECT + SECOND_JSON_OBJECT
BROKEN_FIRST_OBJECT
INCOMPLETE_OBJECT
~~~

## Step 4: parse the recovered object normally

After isolating the complete object, the extension passes it back through native JSON.parse.

Then all normal workspace-operation validation continues.

That distinction matters.

The parser is not deciding whether a file write is safe.

It is only converting one narrowly recoverable transport shape into the same proposal object the security pipeline already understands.

---

# πŸ§ͺ Regression tests

A bug like this should never depend on β€œI think I fixed it.”

The workspace-write validation script now includes parser cases.

The tests cover:

- normal valid proposal JSON
- duplicate trailing closing delimiters
- whitespace between accidental closing delimiters
- JSON-looking braces and brackets inside file-content strings
- rejection of arbitrary trailing prose
- rejection of a second JSON value
- rejection of malformed JSON inside the first object

The same validation suite also continues to cover workspace paths and line patches.

The release pipeline runs:

~~~text
npm ci --ignore-scripts
npm run check
npm run compile
npm run validate:security
npm run validate:workspace-write
npm run validate:webviews
VSIX packaging
~~~

On Linux it also audits production dependencies.

---

# πŸͺŸπŸ§ Cross-platform CI

The extension targets real VS Code machines, and Windows behavior already taught me that platform details matter.

The release workflow therefore validates on both:

- Windows
- Ubuntu

I do not want a path/security change declared safe because it passed only on Linux.

For v2.0.1, the pull request validation passed, then the merged main branch ran the complete release workflow again.

Both operating systems passed.

Only after that did the release job publish the VSIX.

That release is:

~~~text
hermes-gateway-vscode-2.0.1.vsix
~~~

---

# πŸŽ‰ The most important test: repeat the original failure

Automated tests are necessary, but for an interoperability problem I also wanted the exact original workflow repeated against the real Hermes gateway.

After installing v2.0.1, I repeated the create-file scenario.

This time it worked.

That confirmed the full chain:

~~~text
local VS Code context
        ↓
Hermes file attachment
        ↓
Hermes reasoning
        ↓
workspace-write proposal
        ↓
v2.0.1 parser
        ↓
operation validation
        ↓
workspace security checks
        ↓
approval
        ↓
local file creation
~~~

That was the milestone I was aiming for.

---

# 🧯 What did not change in v2.0.1

It is worth being explicit about this.

The parser fix did not remove or bypass any of the important security controls.

v2.0.1 still keeps:

- VS Code Workspace Trust enforcement
- open-workspace containment
- relative paths only
- path traversal rejection
- Windows unsafe-path protections
- symlink traversal rejection
- protected credential/control paths
- operation count limits
- generated-content byte limits
- native diff review
- user approval
- create-file no-overwrite
- stale edit detection
- rollback protections
- conservative undo
- no arbitrary model-facing local shell

The fix made the parser more interoperable, not the filesystem boundary more permissive.

---

# 🧰 The debugging method that worked

There were several moments in this project where the obvious symptom pointed at the wrong layer.

The workflow that worked best was:

~~~text
1. Reproduce one exact user action.
2. Identify the last confirmed-successful boundary.
3. Read the literal error from the next boundary.
4. Inspect the raw data crossing that boundary.
5. Fix the smallest responsible layer.
6. Add a regression test for that exact failure.
7. Re-run the full cross-platform pipeline.
8. Repeat the original manual scenario.
~~~

For the JSON incident:

~~~text
context upload      = success
Hermes reasoning    = success
proposal generation = success
proposal parse      = failure
filesystem validate = never reached
file write          = never reached
~~~

Once that sequence was clear, the solution became much smaller and safer.

---

# πŸ”’ Why I like the local-authority pattern

This project reinforced a design pattern I want to reuse elsewhere.

An AI agent is good at:

- understanding intent
- analyzing code
- generating content
- planning edits
- suggesting commands
- reasoning across context

A deterministic local client is better at:

- knowing the actual workspace roots
- resolving canonical paths
- enforcing filesystem rules
- tracking local file versions
- showing native diffs
- asking for approval
- applying exact mutations
- refusing stale operations

That leads to a clean division:

~~~text
AI decides WHAT it wants to do.
Local policy decides WHETHER and HOW it may happen.
~~~

That is a much stronger model than handing a remote agent unrestricted local authority.

---

# πŸ“Š Current capability snapshot

As of v2.0.1, the extension now supports the following practical workflow.

## Connection and sessions

- visible connection state
- authenticated Hermes gateway access
- existing session loading
- new session creation
- transcript hydration
- session search
- grouping
- pinning
- rename/archive/delete
- multiple gateway profiles
- diagnostics

## Agent interaction

- model selector
- live slash-command completion
- /queue
- /steer

## Context

- current file
- Explorer file
- Explorer folder snapshot
- selected code
- @file composer search

## Workspace editing

- create_directory
- create_file
- edit_file
- apply_patch
- multi-file review
- native diffs
- selective approval
- transaction preflight
- mutation-time checks
- rollback
- undo
- Git dirty warnings
- Workspace Trust

That is a big jump from where the project started.

---

# πŸ›£οΈ What I want to build next

The next major improvements are not about making the bridge more powerful.

They are about making the experience more native.

## 1. Editor-native Hermes actions

I want right-click / Code Action flows for:

- Explain Selection with Hermes
- Fix Selection with Hermes
- Refactor Selection with Hermes
- Generate Tests with Hermes
- Document Selection with Hermes

The selection-context machinery already exists, so these should build on the same safe path.

## 2. Persistent Hermes Changes view

The current proposal review works, but a dedicated view would be better for larger tasks.

Something like:

~~~text
Hermes Changes

A Notes/new-description.md   [Diff] [βœ“]
M src/app.ts                 [Diff] [βœ“]
M README.md                  [Diff] [ ]

[Apply Selected] [Reject] [Undo Last]
~~~

That would move the UX closer to Codex/Copilot-style change management.

## 3. Controlled test/task feedback loop

Eventually I want:

~~~text
Hermes proposes edit
   ↓
VS Code approves/applies
   ↓
approved task/test runs
   ↓
bounded diagnostics return to Hermes
   ↓
Hermes proposes follow-up patch
~~~

I still do not want β€œremote model gets arbitrary shell.”

The first implementation should prefer explicit VS Code Tasks or tightly scoped, previewed commands.

---

# 🧠 Lessons learned

A few lessons from this project are going into my default design philosophy.

## 1. Keep authority close to the resource

Hermes is remote.

The files are local.

VS Code should own local filesystem policy.

## 2. Treat model-generated structured data as untrusted

Even a very good model can emit an extra bracket.

That does not mean accept anything.

It means define narrow, deterministic recovery rules for known failure modes.

## 3. Human-readable labels are not machine coordinates

Keep display labels separate from exact workspace-relative paths.

## 4. Cross-platform semantics matter

Windows path identity cannot always be tested with Unix assumptions.

## 5. Regression tests should reproduce the real bug

The best test is not β€œparser returns something.”

It is:

> Does the exact malformed shape that broke the real workflow now succeed, while nearby unsafe shapes still fail?

## 6. Manual end-to-end validation still matters

CI can prove a lot.

It cannot fully simulate every live Hermes model/gateway/client interaction.

The final retest against the actual gateway was important.

---

# 🏁 Final state

The v2.0.1 release is the first build where I am comfortable saying the core local coding loop works:

~~~text
Context
β†’ Reason
β†’ Propose
β†’ Review
β†’ Approve
β†’ Write
~~~

The important part is that the workflow became more capable without abandoning the security boundary that made the feature acceptable in the first place.

That is the direction I want the project to keep taking:

> More native UX. Better automation. Strong local policy. No invisible expansion of trust.

πŸͺ½πŸ”πŸš€