Files
OpenViking/examples/agent-hook-plugin/README.md
T
t0saki 6fb370cfba fix(memory-plugin): run shell commands that carry viking:// and attach a notice (#5131)
* fix(memory-plugin): split the viking:// URI guard into deny and notice

A shell command that carries a viking:// URI is not necessarily trying to
open it: ov CLI arguments, HTTP payloads and grep patterns all mention one.
The guard used to deny every such command, and models learned to split the
URI to get past it.

evaluateUriGuard now denies only file tools whose path is a viking:// URI.
evaluateUriNotice returns a notice for shell tools instead, naming the
plugin, the replacement tool and telling the model to ignore it when the URI
is intentional. preToolUseOutput wraps both for the PreToolUse hosts.

* fix(memory-plugin): stop treating grep's pattern as a path

pattern was one of the path keys, so Grep(pattern="viking://", path="/repo")
was denied on every host that guards grep, although it only searches local
files for the text. It stays a location for glob, which the generic sweep
still reaches.

* fix(dsh): run shell commands that carry viking:// and attach a notice

bash is no longer denied by tools/pre-execute. A tools/post-execute listener
delegates to the rest of the chain first, then appends a plugin context with
form "notice" when the command carried a viking:// URI, so a later listener's
block or content replacement survives. pluginMessage moves to capture.mjs and
takes the whole source, since a notice needs a summary as well as a form.

* fix(pi): notice on tool_result instead of blocking bash

tool_call now denies only read/grep/find/ls on a viking:// path. A bash
command that carries a URI runs, and tool_result appends the notice after the
result's own content blocks. The shared plugin-config test reads pi's version
from its package.json, like the other harnesses, instead of a literal.

* fix(agent-hook-plugin): trae notices shell commands, cursor stops guarding the shell

TRAE and ZCode use the shared preToolUseOutput, so Bash and RunCommand on
TRAE get additionalContext instead of a deny. ZCode's matcher still names no
shell tool, because its strict output schema is not verified to accept that
envelope.

Cursor has no channel that shows the model a note after a shell command, so
beforeShellExecution is dropped. The installer prunes an entry an older
install left behind, and the guard ignores a shell event that still arrives.

* feat(opencode): notice on tool.execute.after

bash had no guard on opencode. A command that carries a viking:// URI now
gets the notice appended to its output; read/glob/grep keep their deny in
tool.execute.before.

* feat(claude-code): guard Edit/Write and notice on Bash

The PreToolUse matcher grows from Read|Glob|Grep to
Read|Glob|Grep|Edit|Write|Bash. Edit and Write on a viking:// path are denied
like the read tools, and a Bash command that carries one gets
additionalContext. The script is now just preToolUseOutput, so the shared
library drops the guarded option that only this script used.

* feat(codex): add the PreToolUse URI guard

Codex gets the same uri-guard script as claude-code on a Bash matcher. Its
Edit and Write matchers are aliases for apply_patch, whose input is a patch
body with no path to deny, so the hook only ever adds a notice. The doctor
expects the sixth hook trust record, and users approve it once in /hooks.

* docs: capability reference rows for the deny/notice guard
2026-09-17 17:25:20 +08:00

4.3 KiB

OpenViking Memory for the config-driven hook hosts

Cursor, TRAE, TRAE CN and ZCode all install the same way: the shared installer writes lifecycle hooks and an MCP server entry into the host's own configuration files, and assembles the OpenViking runtime beside the integration. None of them has a marketplace listing to register or a separate MCP setup to do.

bash examples/memory-plugin-shared/install.sh --harness cursor
bash examples/memory-plugin-shared/install.sh --harness trae,trae-cn
bash examples/memory-plugin-shared/install.sh --harness zcode

Requires an OpenViking server with viking://~ home-alias support. Recall targets the caller's own context space through viking://~/memories and viking://~/skills; the uid-less viking://user/memories shorthand is rejected by newer servers.

What the hooks do

  • Session start — injects the user profile and preferences into context, and replays anything an offline session queued.
  • Prompt submit — searches OpenViking for memories relevant to the prompt and injects them, deduplicated by event id and a 500ms window.
  • Tool use — denies local file tools a viking:// virtual path and points the agent back at the OpenViking MCP tools. On TRAE a shell command that carries a viking:// URI still runs, with a notice pointing at the same tools.
  • Stop — captures the finished turn and commits the OpenViking session. Cursor also runs this before a compaction and at session end; ZCode answers first and finishes the writes in a detached worker.

Layout

scripts/hook.mjs is the single entry every hook command runs. It owns the state machine all four clients share — the debounce, the prompt dedup, the recall cache, the cross-process lock — and asks the adapter under hosts/ for the four things that differ: the event vocabulary, the response envelope, how a prompt is read out of the payload, and how a finished turn is captured. scripts/uri-guard.mjs and servers/mcp-proxy.mjs are likewise one file each, with the host chosen from the client id the installer passes.

The root plugin.json is host-neutral package metadata used for version checks and diagnostics. It is not a Claude Code, Cursor, TRAE, or ZCode native plugin manifest.

hosts/<host>/ holds only what a host reads as configuration — hooks.json, .mcp.json, openviking.integration.json, plus Cursor's rule and skill. Everything executable stays one level up, because ../../memory-plugin-shared/lib is the path that resolves both in this repository and in an installed ~/.openviking/agent-integrations/<client>/.

The memory logic itself is not here: recall, batching, the pending queue, credential resolution and the MCP proxy all come from examples/memory-plugin-shared/lib, which the installer copies to ~/.openviking/agent-integrations/memory-plugin-shared/lib.

Host notes

  • Cursor — six events, including the preCompact and sessionEnd no other host in this plugin has. Commits on Stop once capturedSinceCommit reaches the threshold, and unconditionally before a compaction. Sessions are cu-. See the Cursor guide.
  • TRAE / TRAE CN — capture reads prompt, text_content and last_assistant_message off the Stop event rather than parsing a transcript. Every Stop that carries content commits. Sessions are tr- and trcn-. See the TRAE guide.
  • ZCode — the rollout file is the authoritative incremental transcript: stable host turnId values drive deduplication and let a later Stop recover missed turns, and hook stdin is only the fallback. ZCode supports neither PreCompact nor SessionEnd, so committing on every Stop stands in for both. Its output schema is strict, so a pass-through writes nothing at all. Sessions are zc-. DESIGN.md records the verified extension surface.

Diagnostics

node ~/.openviking/agent-integrations/<client>/scripts/ov-memory-doctor.mjs --offline

The client defaults to the one this copy was installed for; pass cursor, trae, trae-cn or zcode as an argument to override it, drop --offline to probe the server as well, and add --json for a machine-readable report.

Tests

node --test examples/agent-hook-plugin/tests/*.test.mjs