feat(continue): add native Continue plugin (CLI hooks)#111
feat(continue): add native Continue plugin (CLI hooks)#111theyavuzarslan wants to merge 2 commits into
Conversation
Adds a Continue integration parallel to the Hermes and Cline adapters, so
AgentGuard can gate every tool call in the Continue CLI (`cn`) before it
runs. Targets the CLI's Claude-Code-compatible hook system documented at
continuedev/continue/extensions/cli/src/hooks/types.ts. The VS Code / JetBrains
extension does not yet expose a tool-call hook surface, so this PR scopes
to the CLI; an extension-side PR would need an upstream Continue change.
Surfaces:
- ContinueAdapter (src/adapters/continue.ts) maps Continue tool names
(run_terminal_command, create_new_file/edit_existing_file/single_find_and_replace
/multi_edit, read_file/read_file_range/read_currently_open_file, fetch_url_content,
search_web) to the shared HookAdapter contract. Out-of-scope tools
(grep_search, file_glob_search, ls, codebase, MCP server tools) return null
so AgentGuard does not block third-party tools it cannot reason about.
- File-hook bridge (skills/agentguard/scripts/continue-hook.js) handles
Continue's stdin JSON / stdout JSON protocol. Emits Continue's documented
`{ hookSpecificOutput: { hookEventName, permissionDecision, ... } }`
shape — deny / ask / allow map 1:1 with no lossy translation.
- `agentguard init --agent continue` drops the skill at
~/.continue/skills/agentguard and merges PreToolUse / PostToolUse entries
into ~/.continue/settings.local.json. Pre-existing hooks are preserved;
re-running is idempotent.
Review lessons applied (carried over from the Cline plugin review):
- No spawnSync shim. The settings.json `command` is `node <absolute-path>`,
pointing directly at the bundled bridge. One process boundary.
- Path resolution uses fileURLToPath() + dirname() (no string-replace),
and only attempts the bundled engine when its path exists.
- multi_edit batches scan every edit's filepath and elevate any path that
isSensitivePath flags as the envelope's primary, so a batch of
[src/foo.ts, .env.production, src/bar.ts] cannot smuggle the .env target
past the engine. Adapter tests lock the contract in.
- Fail-closed by default; single env var AGENTGUARD_CONTINUE_FAIL_OPEN=1
inverts. No cross-surface divergence to maintain.
Wiring:
- 'continue' added to AgentInstaller, RuntimeAgentHost, AgentGuardAgentHost,
SUPPORTED_AGENT_INSTALLERS, AUTO_AGENT_DETECTION, normalizeAgentHost.
- resolveCronAgentHost narrowed to the cron-capable subset so the new host
doesn't leak into cron backends that don't target it (mirrors the Cline fix).
Tests: 446/446 pass (was 418 on this branch's base; +28 new — adapter unit
covering parseInput / mapToolToActionType / buildEnvelope / multi_edit
worst-case, hook E2E covering allow/deny/ask, out-of-scope passthrough,
post-tool audit, missing-field block, fail-closed default, fail-open
override, invalid-stdin prompt exit; installer tests covering fresh
install, preserving prior hooks, idempotency).
End-to-end verified against a fresh temp .continue dir:
- run_terminal_command rm -rf / -> permissionDecision: "deny"
- run_terminal_command echo hello -> {}
- create_new_file /project/.env -> permissionDecision: "ask"
AgentGuard PR ReviewI found a few concrete issues in the Continue integration patch.
|
…t, cron)
Four issues raised in review; all addressed in this commit.
1. MEDIUM — installer used JSON.stringify(hookScriptPath) for the hook
command, which is JSON quoting, not shell quoting. On a path with
spaces, single quotes, or backslashes the resulting `node "..."` could
parse incorrectly when Continue's shell-based hook runner spawns it.
Fix: continueHookCommand() now POSIX-single-quotes the path (with the
classic '\\'' close-escape-reopen trick for embedded apostrophes) on
non-Windows, and double-quotes on win32 with a clear error if the path
contains a double quote (NTFS doesn't allow them in paths anyway).
Dedup also relaxed to substring-match on `continue-hook.js` so an old
JSON-quoted entry from a prior install is recognized and replaced
rather than appended-alongside.
2. HIGH — continue-hook.js could fall open on malformed in-scope payloads
when AGENTGUARD_CONTINUE_FAIL_OPEN=1 was set, because the validation
error path checked FAIL_OPEN before blocking. That meant a half-formed
`run_terminal_command` (no `command` field) with FAIL_OPEN=1 was being
allowed. The fail-open override exists for engine-unavailable cases,
not for "we don't know what we're being asked to allow."
Fix: malformed in-scope payloads and unparseable stdin now block
regardless of FAIL_OPEN. Validation tightened: multi_edit edits[] is
walked entry-by-entry and rejected if any entry isn't an object or is
missing a filepath (with no top-level fallback); fetch_url_content URL
is parse-checked; file-bearing tools reject NUL bytes; shell command
capped at 64 KiB.
3. MEDIUM — ContinueAdapter.buildEnvelope dropped multi_edit edit content
(only paths made it into actionData). Policy that wants to reason about
destructive batches (e.g. detect `old_string=non-empty, new_string=''`
as a delete) had nothing to work with.
Fix: actionData now includes `operations: [{path, kind, old_preview,
new_preview, is_delete}]` normalized across multi_edit /
single_find_and_replace / create_new_file. Previews are clamped to
≤256 chars + ellipsis so the envelope stays bounded.
4. MEDIUM — resolveCronAgentHost silently narrowed `continue` to
undefined. The behavior was correct (continue has no agent-managed
cron backend, system cron picks up the slack) but the silent narrow
meant users couldn't tell why their cron command had no host-specific
behavior.
Fix: split into two helpers. resolveConfiguredAgentHost() always
returns the raw host. resolveCronBackendHost() narrows for callers
that need a CronAgentHost. The two cron entry points (`disconnect`,
`subscribe`) now call noteCronBackendFallbackIfNeeded(config) which
emits a one-line stderr note when a configured host has no
agent-managed backend — once per process. The old name
resolveCronAgentHost is retained as a back-compat shim.
Tests: 455/455 pass (was 446 before; +9 new — shell-quoting + path-with-
spaces + stale-format dedup in installer; FAIL_OPEN does not bypass
validation; multi_edit non-object edits[] rejection; fetch_url_content
URL parse rejection; multi_edit operations envelope including
is_delete; preview clamp; create_new_file create-op surface).
End-to-end verified against /tmp/continue\ space\ test/ — command field
in settings.local.json is `node '/tmp/continue space test/.../continue-
hook.js'` (POSIX-shell-safe), and a deletion-style multi_edit batch is
correctly elevated to `permissionDecision: "ask"` with sensitive_path
surfaced.
|
Thanks for the review. All four items addressed in `c74e6ee`:
Tests: 455/455 pass (was 446 before fixes; +9 new — shell-quoting + path-with-spaces + stale-format dedup in installer; Happy to split into smaller commits if you'd prefer. |
Summary
Adds a Continue (continuedev/continue) integration parallel to the Hermes and Cline adapters, so AgentGuard can gate every tool call in the Continue CLI (
cn) before it runs.Continue's CLI ships a Claude-Code-compatible
PreToolUse/PostToolUsehook system atextensions/cli/src/hooks/— stdin JSON in, stdout JSON out, withpermissionDecision: \"allow\" | \"deny\" | \"ask\". This PR wires AgentGuard's engine to that surface.Scope note: The VS Code / JetBrains Continue extension does not currently expose a tool-call hook surface — it only has soft
rulesandToolOverride(all-or-nothing per-tool disable). So this PR targets the CLI only; an extension-side PR would need an upstream Continue change first.What changed
src/adapters/continue.ts—ContinueAdaptermapping Continue tool names (run_terminal_command,create_new_file/edit_existing_file/single_find_and_replace/multi_edit,read_file/read_file_range/read_currently_open_file,fetch_url_content,search_web) to the shared adapter contract. Out-of-scope tools (grep_search,file_glob_search,ls,codebase, MCP-server tools) return null so AgentGuard doesn't block third-party tools it can't reason about.skills/agentguard/scripts/continue-hook.js— file-hook bridge emitting Continue's exact decision shape:{ \"hookSpecificOutput\": { \"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"deny\", \"permissionDecisionReason\": \"...\" } }deny/ask/allowmap 1:1 to AgentGuard'sblock/require_approval/allow— no lossy translation.agentguard init --agent continue— drops the skill at~/.continue/skills/agentguardand mergesPreToolUse/PostToolUseentries into~/.continue/settings.local.json. Pre-existing user hooks are preserved; re-running is idempotent.'continue'wired throughAgentInstaller,RuntimeAgentHost,AgentGuardAgentHost,SUPPORTED_AGENT_INSTALLERS,AUTO_AGENT_DETECTION,normalizeAgentHost.resolveCronAgentHostnarrowed to the cron-capable subset.Lessons applied from #110 review
spawnSyncshim. The merged settings.jsoncommandisnode <absolute-path-to-bundled-script>. Single process, no stdin-passthrough question.fileURLToPath() + dirname()(no string-replace), and only loads the bundled engine when its path actually exists.multi_editbatches scan every edit's filepath and elevate any path thatisSensitivePathflags as the envelope's primary, so a batch of[src/foo.ts, .env.production, src/bar.ts]cannot smuggle the.envtarget past the engine. Adapter tests lock the contract in.AGENTGUARD_CONTINUE_FAIL_OPEN=1inverts. No cross-surface divergence.Install / use
```bash
npm i -g @goplus/agentguard
agentguard init --agent continue
```
Restart Continue (`cn`). Hooks fire on every tool call.
Test plan
References:
🤖 Generated with Claude Code