venira-mcp
Fast, workspace-scoped MCP server for coding agents (OpenCode, Claude Desktop, any MCP client).
Two tool modes — Default (32 coding tools + shell, no CI/CD) · Expert (43 tools: default + CI/CD). Setting VENIRA_ALLOW_JS_EXEC=1 adds project_execute_js (33 / 44).
Pure Node, 1 native dependency (better-sqlite3), no GUI, no external binary.
Security scope. Venira is a guard rail with an audit trail, not a sandbox. It runs with the privileges of the OS user that started it. The workspace lock limits the paths the file tools can touch (unless VENIRA_UNRESTRICTED=1), not what a shell command does: commands are classified by rules (allow / ask / deny) and every call is audited, but a malicious prompt can still steer the agent into running a harmful command that the active profile lets through (with off, every command runs). Anything running as the same OS user can read the keys in VENIRA_DATA_DIR. For untrusted workspaces, or whenever you need a real isolation boundary, run Venira in a container or VM. Details: SECURITY.md.
1. Setup
One-line install (run in your project, then restart the client):
Prefer your agent? Paste this prompt into OpenCode/Claude/Cursor:
Manual config (pick your client):
All use the same npx command. See §2 for ready-to-copy JSON/TOML.
In this repo .vscode/mcp.json is git-ignored because it holds machine-specific settings (and may relax security flags). Copy .vscode/mcp.example.json to .vscode/mcp.json and adjust it locally.
2. Client configs (copy-paste)
The examples use four environment variables: workspace, tool set, output size, and security posture. For most users, change only VENIRA_WORKSPACE and keep VENIRA_SECURITY_PROFILE=balanced.
OpenCode / VS Code / Cursor / Claude / Kimi / DeepSeek / Antigravity — use the wrapper format supported by your client:
Generic MCP JSON example
For clients that accept the common mcpServers JSON shape, use the example for the user's operating system as a realistic starting point. Replace the example paths with paths on the user's machine; VENIRA_DATA_DIR is optional and can point to any writable directory. Keep this file private if it contains local paths or credentials.
Windows:
Linux:
mcpServers is a common client configuration shape, but some clients use a different outer key or file format. Preserve the Venira command, args, and environment values when adapting the example. The approval command returned at runtime uses the effective data directory from this configuration, so users and agents should copy data.approval.approve_command instead of constructing a path themselves.
Codex (~/.codex/config.toml):
Security profiles
VENIRA_SHELL_MODE and VENIRA_JS_EXEC_APPROVAL are advanced overrides and take precedence over the profile. VENIRA_ALLOW_JS_EXEC=1 separately registers the JavaScript tool; it remains disabled by default.
Choose trusted when you want fewer prompts on a personal machine. Choose off only when you intentionally accept that shell checks and per-call JavaScript approval are disabled. The secret_env gate remains active in every profile.
Advanced configuration
See the full environment reference in §6 for caching, shell, provider, and diagnostic options.
3. Benchmark (per-op, p50)
Measured: Windows + Node 24, standalone build, 2026-11-01.
Batch: 8 reads as one batch call (16.6 ms) vs 8× individual (~76 ms) — the round-trip saving
that makes the agent loop fast.
Agent tool-path benchmark: Venira MCP vs Codex exec_command
Measured on 2026-11-07 on one Windows 11 build 26200 machine (Intel Core i5-1035G1,
8 logical processors, 11.8 GiB RAM, Node v24.20.0). Each workload used 20 paired
samples per phase, alternating call order. Venira used the persistent MCP connection;
each native exec_command call launched a fresh PowerShell process. Cache-miss samples
cleared Venira's in-process cache; warm samples reused it. The OS filesystem cache stayed
warm.
Both workloads matched exactly in all 20 pairs: normalized text for the 8-file batch and
sorted file/line locations for search. These are practical agent tool-path timings, not
raw filesystem timings. A separate 100-sample component benchmark shows native
PowerShell can be faster for simple uncached reads; the largest Venira gains are on warm
list/search and avoiding a fresh shell process. The p95 estimates are rough with 20
samples, and results from this one machine are not a general performance guarantee.
See the full methodology and results and
raw samples. Reproduce the component benchmark from the
repository root with npm run bench:tools -- --runs=100.
4. Two tool modes
Two tool modes set which tools the client sees. Set via VENIRA_TOOL_MODE env var or
"toolMode" in venira.config.json (env wins). VENIRA_PROFILE=core|full still applies on top.
project_delete_path is always available. In balanced / strict mode, shell rm -rf / / ~ and Windows drive-wide deletes are hard-denied, and recursive/force deletes (Remove-Item -Recurse -Force, rd /s /q, del /f /q) need approval. permissive mode applies no shell checks at all.
5. Tools quick reference
All tools return the standard envelope (ok, kind, data, hint, elapsed_ms).
Read (batch-safe, cached)
Write (mutating, pass reason)
Checkpoints (git-free rollback)
project_snapshot — create/list/restore/delete. Content-addressed, deduplicated, atomic per-file restore.
Code intelligence
Shell (async, policy-gated)
Git provider: PR/CI (GitHub, GitLab, Bitbucket)
project_pr_create/comment/merge/list/close/reopen, project_ci_status/trigger/logs/wait, project_ci_control (cancel / retry-failed; Bitbucket supports cancel only) — same tool args across providers; token from project_secret or env vars.
CI reads are bounded and never hide a cap:
- Pagination — status/job/check-run/step lists follow the provider's next-page link (
Link header on GitHub/GitLab, body next on Bitbucket) for at most 5 pages / 200 items, stay on the configured API origin, and never fetch the same page twice. When a cap or the provider total stops the walk, the result says so (truncated / partial) instead of reporting a partial list as complete.
- Verdicts — a finished Bitbucket pipeline/step is read from
state.result (not just COMPLETED), so a failed run is never shown green.
- Logs — failing steps/jobs are read first; archives and entries are byte-capped (see
VENIRA_GIT_LOG_*), and a log whose declared size is over the cap is refused before it is downloaded.
project_ci_wait — one bounded long-poll (no polling loop). Returns summary (one line), next_step (wait_again · merge_or_continue · fix_failing_step_then_push · read_logs · inspect_status), failing_jobs (max 5) and, on failure, a failure_tail of the first failing job (numeric ids on GitHub/GitLab, UUID step ids on Bitbucket).
Secrets (encrypted at rest)
project_secret — store/inspect/remove credentials by name; AES-256-GCM; rotation metadata; expired secrets refused.
project_github_connect — GitHub Device Flow, tokens in vault.
Diagnostics & planning
project_explain_error — turn an error into a cause + next tool calls (rule table, no model).
project_plan_read_batch — plan which files to read for a task, returns ready project_read_batch call.
JS execution
project_execute_js — run synchronous JS (opt-in, VENIRA_ALLOW_JS_EXEC=1); per-call human approval by default.
6. Environment variables (key ones)
Full reference: the source reads 90+ distinct VENIRA_* names; this table lists the documented ones. See the source or npx venira-mcp --help for the rest.
7. Security model
Conservative by default:
- Workspace lock: all paths validated inside
VENIRA_WORKSPACE; ../ escape rejected (path_escape).
- Security profile (
VENIRA_SECURITY_PROFILE, default balanced): sets shell policy and per-call JS approval together. VENIRA_SHELL_MODE and VENIRA_JS_EXEC_APPROVAL can override either setting individually. Every project_shell_run command is classified allow, ask or deny.
balanced (default): hard-deny rules refuse the command; ask rules need approval; routine dev-workspace commands run; anything unclassified needs approval.
strict: like balanced, but only a read-only / workspace-local allowlist runs without approval.
permissive (the trusted profile): ask-rule commands run without a prompt; a hard-deny match becomes an approval request (ask.deny.<rule>).
off: the shell policy is disabled — every command runs, nothing is asked or denied (only secret_env injection still asks). Prints a startup warning; use only in a trusted environment. Previous permissive behaviour (v1.14.4) = off.
- Hard-deny (14 rules): disk destroy, filesystem mount/overwrite, shutdown/reboot, fork bomb, pipe download to shell, privilege escalation (sudo/doas/runas), filesystem root/drive-home delete (
rm -rf /, rm -rf C:\, Remove-Item -Recurse -Force $HOME, …), chmod 777 /, Windows drive-wide and wildcard mass delete, encoded PowerShell commands, self-approval (touching the approve CLI or approval store), protected credential/key files.
- Ask (20 rules): command substitution, reads/writes/
cd outside the workspace, network access, package mutation, git history rewrite, container/VM control and state, process control and taskkill /f, secret dumps, dynamic execution and wrapper commands, find -delete/-exec, destructive recursive/force deletes (Remove-Item -Recurse -Force, rd /s /q, del /f /q; prefer project_delete_path), attribute reset, infinite writes to NUL, unresolved file arguments.
- Deny always wins over ask.
- Secrets: AES-256-GCM encrypted at rest; values never in tool metadata;
secret_env requires human approval; expired secrets refused.
- Provider DLP: Secretlint scans PR/CI request args and HTTP payloads; detected credentials blocked before sending, withheld from response.
- Audit trail: all ops (including failures) written to SQLite WAL;
project_read_audit for post-mortems.
- Token transport: provider token in
Authorization header only; dropped on cross-origin redirect.
Approvals — what needs a human, and how it is granted
Granting a shell or JS approval:
- The call fails with
[approval_required] and returns data.approval_id (valid for 5 minutes) together with the command or code, the rule and the reason. Shell and JS share one payload, data.approval: { approval_id, gate, rule, reason, subject_preview, expires_at, approve_command, retry_with: { tool, arguments } }.
- A person copies
data.approval.approve_command exactly into an interactive PowerShell terminal (the returned command includes the server data directory and works in Windows PowerShell and PowerShell 7), reviews the request and answers y. The CLI refuses without a TTY. --yes skips the prompt. --session extends the grant to the same command group until it expires, but only for rules in VENIRA_SESSION_GRANT_RULES (built-in: cd_outside_workspace).
- The agent retries the same call with
approval_id. The grant is HMAC-signed, tied to the session, bound to the exact command / shell / cwd / secret_env / rule (or code / argv for JS), and single-use. Concurrent retries of the same id cannot both run: consumption is an atomic file claim, so exactly one wins.
TTL behavior: the pending request (approval_id) is valid for a fixed 5 minutes and cannot be revived by retrying. A --session grant lasts 10 minutes by default (VENIRA_SHELL_APPROVAL_TTL_MS) and lives only in this server process; restarting the server clears it.
Agent helpers:
project_shell_run with dry_run: 1 returns the tier, rule and would_run / requires_approval without running anything or creating an approval, so an agent knows beforehand which command will be asked.
approval_wait_ms (max 45000) on project_shell_run / project_execute_js keeps the call open while the human approves; a grant that arrives in time runs the command in the same call, otherwise you get the normal [approval_required] (the pending approval_id stays valid). Keep it below the client's request timeout.
project_health reports approvals (mode, JS gate, elicitation, session rules, TTL, max wait), the shared posture report (level, stable finding ids such as workspace.unrestricted, profile.off, combo.unrestricted_open_shell, js.exec_without_approval) and the flat warnings list. Posture findings are warnings only; invalid values already fail closed at startup. doctor surfaces the same report.
Alternative (off by default, project_shell_run only): VENIRA_SHELL_ELICIT=1 shows a confirmation dialog in MCP clients that support elicitation, and accepting it records the grant for you.
Removed in v1.15: approval_token and VENIRA_SHELL_AUTO_APPROVE_TOKEN. Passing approval_token now returns [approval_invalid] with a hint to use approval_id; setting the env var only logs a warning and is ignored.
Approvals are a guard rail, not a sandbox: a process running as the same OS user can read the approval key.
Snapshot store (checkpoints)
Checkpoints live outside every workspace, in one shared per-user folder, so every MCP client that opens the same project sees the same history:
- Folder name = workspace folder name. The first workspace to claim a name keeps it (recorded in
project.json); a different workspace with the same name gets a -<hash8> suffix. The same workspace always maps to the same folder, whatever the path casing or 8.3/symlink spelling.
- Override with
VENIRA_SNAPSHOT_ROOT (shared root) or VENIRA_SNAPSHOT_DIR (exact folder, single project). Both must be absolute paths; a relative one fails with [config_invalid].
- The file tools (read, write, edit, search, delete) refuse anything under the snapshot root or exact dir, the same way they refuse the data dir. Only
project_snapshot touches the store.
- Retention is per project (
VENIRA_SNAPSHOT_KEEP), so one busy project cannot push out another's checkpoints.
- Shell commands are guarded too: any read, write or delete operand inside the snapshot store is a hard deny in
balanced / strict (deny.protected_file_access) and needs approval in permissive. off disables shell policy entirely, so keep checkpoints you cannot afford to lose in a separate backup as well.
- Unreadable checkpoints are reported, never dropped silently.
project_snapshot action=list adds unreadable[] (snapshot_version_unsupported = written by a newer venira-mcp, snapshot_corrupt = broken index file) and a hint. While any index is unreadable, blob cleanup is paused (gc_skipped: "unreadable_manifests" on delete), because that index still owns blobs this build cannot enumerate. For the first case upgrade venira-mcp; for the second, remove the broken index\<id>.json.gz yourself (agent shell access to the store is denied by design) and cleanup resumes.
- Manifests carry a
version. Older layouts are upgraded in memory when read (MIGRATIONS in lib/snapshot-store.ts); the file on disk is never rewritten.
Backing up the store
The store is plain files: immutable content-addressed blobs\<ab>\<sha256>, gzip manifests in index\ written by write-then-rename, and project.json naming the owning workspace. A backup is a folder copy; a restore is copying it back to the same location (a checkpoint only restores into the workspace recorded in its manifest).
- Run the copy yourself, outside the agent: shell policy denies the store to agent commands.
- Copy the shared root (
VENIRA_SNAPSHOT_ROOT) to cover every project, or one project folder. Windows: robocopy "%LOCALAPPDATA%\venira\snapshots" "D:\backup\venira-snapshots" /E; Linux/macOS: rsync -a ~/.local/share/venira/snapshots/ /mnt/backup/venira-snapshots/ (macOS: the Application Support path above).
- Do it while no agent is creating or deleting checkpoints. If you copy in two passes, take
index\ first and blobs\ second: a blob is always written before the manifest that references it, so that order cannot capture a manifest without its blobs, while the reverse order can. Blobs without a manifest are harmless leftovers.
- After restoring a backup,
project_snapshot action=list should show the expected count and no unreadable entries.
Mirroring the store (optional)
Set VENIRA_SNAPSHOT_MIRROR_DIR to an existing absolute folder and venira keeps a second copy there, with no cloud SDKs, credentials or network code of its own (point it at a OneDrive/Dropbox folder, NAS share, USB drive or rclone mount and let that tool move the bytes).
- Same layout as the store (
<mirror>\<project>\project.json, index\, blobs\), so restoring is copying the folder back to the store location; there is no separate format.
- After every
project_snapshot action=create the result carries mirror (status, copied_blobs, copied_indexes, bytes, failed[], error). It is best-effort: a mirror problem never changes the checkpoint's own status. project_snapshot action=mirror runs the same sync on demand; add verify=1 to re-hash every blob already at the mirror and re-copy bad ones.
- Order is blobs, then indexes, then
project.json, so the mirror never holds a checkpoint whose blobs are missing. Every copy is verified (blob sha256 equals its file name; index bytes equal the source) before it gets its final name. A corrupt local blob blocks only the checkpoints that use it.
- Append-only: venira never deletes anything at the mirror. A checkpoint pruned locally (
VENIRA_SNAPSHOT_KEEP) or deleted stays at the mirror until you remove it by hand, so expect it to grow; the only overwrite is verify=1 replacing a file that no longer matches its source.
- Bounded: a run stops between files once its time budget is spent and reports
partial / timed_out; run it again to resume (only missing files are copied). A first sync of a large store may need a few runs.
- Refused (
[config_invalid]): a relative path, a folder that does not exist (venira never creates the mirror root, so an unmounted drive is not silently replaced by a local folder), and a folder that equals, contains or sits inside the local store or sits inside the workspace. A mirror folder already owned by a different workspace fails with [mirror_conflict].
- The mirror folder is a protected root like the store: file tools refuse it and shell commands that read, write or delete inside it are denied.
- To restore after losing the store: copy
<mirror>\<project> back to the store location (for example %LOCALAPPDATA%\venira\snapshots\<project>) with venira stopped, then project_snapshot action=list. Unreadable local indexes are not mirrored (they are counted in skipped_unreadable).
Threats this does NOT protect against: a malicious prompt can still instruct the agent to run destructive commands. Use a container/VM for untrusted workspaces — see ISOLATION.md for Docker, WSL 2 and Windows Sandbox setups, including mounts, network controls and their limits.
8. Envelopes & error codes
Success:
Failure:
ok describes the tool operation. For project_shell_run, a command that ran and exited non-zero still has ok: true; command_status: "failed" and hint report the command outcome. A running or cancelled command reports command_status: "running" or "cancelled". A read batch with failed items reports top-level ok: false, status: "partial", and error.code: "batch_partial_failure"; inspect data.responses for each result.
In compact mode, shortened payloads set trimmed: true and truncated: true; summary describes omitted content. For an otherwise unsupported large payload, data.preview_text contains a bounded JSON-text preview and summary reports the original size and shape. Large error messages or attached error data are also explicitly marked as trimmed. Use full when the complete payload is required.
Common codes: not_found, invalid_request, old_text_not_found, old_text_ambiguous, stale_content, path_escape, resource_limit, sensitive_result, unsupported, internal.
9. Agent instructions (paste into AGENTS.md / CLAUDE.md)
10. Development
Adding a tool:
- Register in
tools/project.ts → registerProjectTools, use the config() helper.
- Read-only: route through
executeCached; mutations: add to MUTATING set.
- Envelope: use
buildEnvelope / errorEnvelope from lib/output.ts.
- Add a check in the matching section module under
scripts/verify/ (scripts/verify.ts only orchestrates); update the decision tree in server.ts INSTRUCTIONS.
npm run build && npm run verify && npm run bench:mcp — paste numbers in the PR.
11. Troubleshooting
License
Licensed under the GNU Affero General Public License v3.0 (AGPLv3) — see LICENSE.
Free for developers, individual use, and open-source projects. Embedding Venira into proprietary closed-source services or commercial agent platforms requires a commercial license from the author.