Security Model
Contents
- Core Principle
- Default Safety Posture
- Permission Classes
- Approval Required Actions
- Protected Paths
- External Skill Trust Model
- Shared Catalog Trust Boundary
- Capability Fabric Trust Boundary
- External Skill Security Scan
- npm Package Risk Model
- Hook Enforcement
- Secrets Handling
- Local Web Server
- Private One-Shot File Context
- Owned Process Identity and Containment
- Audit Log
Core principle
The tool runs locally and may access source code, files, shell commands, AI coding CLIs, and external connectors. Therefore, it must default to least privilege.
Default safety posture
- local-first
- read-only until user approves writes
- no public network exposure
- no silent package installation
- no source upload by default
- no auto-push
- no auto-merge
- no auto-deploy
Permission classes
read_workspace
write_workspace
read_external_source
network_access
shell_execute
install_dependency
modify_ci_cd
modify_auth
modify_security
push_code
open_pr
deploy
Approval required actions
Approval is required before:
- installing dependencies
- running unknown scripts
- modifying CI/CD
- changing authentication or authorization
- changing payment, billing, or security logic
- deleting files
- pushing commits
- opening pull requests
- deploying
- enabling external skills
- granting network/filesystem/credential access
Protected paths
Default protected paths:
.env
.env.*
**/secrets/**
**/credentials/**
.github/workflows/**
infra/**
terraform/**
k8s/**
auth/**
payments/**
billing/**
External skill trust model
External skills are untrusted until verified.
External skill → draft
Reviewed skill → experimental
Validated skill → verified
Old or unsafe skill → deprecated
Skills requiring shell, network, write access, or credentials must be explicitly approved.
Dispatch-time skill acquisition
Before agent dispatch, VibeFlow searches only configured pinned registry caches for an
exact verified candidate. Building the approval card is read-only and performs no network
or catalog/lock mutation. The card shows bounded identity and security scan status, never
cache paths or registry credentials. --yes, an interactive TTY answer, or a Web UI card
may approve installation; non-TTY execution without consent skips acquisition without
hanging. HIGH/CRITICAL findings remain blocked, and approved candidates pass through the
authoritative install-time scan again. Approval is not review proof and does not promote
trust. Rejection or failure preserves a skill gap and agent dispatch continues.
Vendor registry cache (read-only)
Installed vendor registries live at ~/.vibeflow/skill-registries/<url-hash>/ and are
identity-pinned by a detached commit OID plus a deterministic SHA-256 bundle hash over
all regular files in the installed skill directory. The bundle hash is computed during
vf skills registry install and stored in the lock file.
The vendor registry cache is read-only for agent file writes. VibeFlow’s PreToolUse
hooks block Write/Edit/patch events targeting paths inside this cache. Agent
writes through the cache are blocked with a critical risk verdict.
Escape hatch
To update or reinstall a registry skill:
vf skills registry update <id> --yes
vf skills registry install <id>/<skill> --on-collision=replace --yes
verify-sync bundle hash check
vf skills verify-sync --from-registry compares each installed skill’s bundle hash
against the lock entry. On mismatch, it reports an error stating the skill was modified
and prints the exact reinstall command. No automatic overwrite or restore is performed
in V1 — explicit reinstall is the only recovery path.
V1 limitation: no shell-command mutation detection
Detecting shell commands that write into the vendor cache (e.g. cp, echo >, tee)
is deliberately excluded in V1. Shell-command mutation detection is bypass- and
false-positive-prone. Existing workspace/outside warnings remain unchanged. This
boundary is documented in src/hooks/risk.ts.
Registry release approval boundary
vf skills registry release approve <proposal-id> --yes checks the active identity and canonical
repository/base branch for each allowlisted target before creating an isolated checkout. It rejects
target pin drift, enforces a lock-only diff, runs vf verify before commit, push, or PR creation,
and emits sanitized evidence. Per-target failures continue; partial failure exits 1.
Shared catalog trust boundary
The shared skill catalog at ~/.vibeflow/skills/ is machine-wide: a skill promoted
in one project becomes available to every project on the same machine. This is a
deliberate tradeoff — re-discovery per project is eliminated, but trust granted in
project A extends to project B without a second review.
Mitigation: the security-scan gate (see below) blocks a skill with HIGH/CRITICAL
findings from being promoted to verified, so an untrusted skill (Context7
unauthenticated fallback, find-skills HTTP, community import) cannot silently become
trusted machine-wide. The gate is optional (degrades gracefully when the scanner is
absent), so when it is not installed, only manually-reviewed skills should be
promoted to verified.
Capability fabric trust boundary
Capabilities are typed manifests and adapters, not arbitrary browser plugins. The AI-first Home can propose capability actions and draw from the typed registry, but it never loads untrusted plugin JavaScript into the browser. Approved capability execution stays inside VibeFlow’s local service and owned dispatch runtime, where it can be audited before launch and rejected if the typed authority does not match.
Curator shared synchronization
Curator scans are local-first. vf skills curator scan and
vf skills curator scan --scope=local write only an ignored local findings report;
they do not run Git, contact a remote, create issues, or publish findings.
vf skills curator scan --scope=repo requires a clean checkout and anchors the
scan to immutable HEAD. It still does not contact a remote. Add --sync to
preview the only possible shared operation. The preview names remote origin,
ref refs/notes/vibeflow-curator, sent fields, excluded fields, visibility risk,
and the exact --scope=repo --sync --yes confirmation command.
Only --scope=repo --sync --yes fetches and pushes that exact Git notes ref.
The note stores a commit OID, finding type, and SHA-256 fingerprint. It never
stores finding detail, finding keys, paths, source content, URLs, usernames, or
credentials. Remote readers may still infer that a matching finding existed, so
do not enable sync where that fact is sensitive.
Git notes are duplicate-reporting hints, not a security attestation. A collaborator with Git write access can add a marker. Markers never approve a change, resolve a finding, or bypass a security gate. GitHub Issues may project human-readable reports later but are not curator deduplication authority.
External skill security scan (optional)
vf skills verify <name> promotes a local skill to verified. Before the status is
written, VibeFlow runs an optional static scan via NVIDIA SkillSpector
(https://github.com/NVIDIA/skillspector) over the skill directory:
skillspector scan <dir> --no-llm --format json --baseline <path>
- Optional dependency. If
skillspectoris not onPATH, promotion still proceeds and is flaggednot-scanned— the gate never hard-blocks on a missing optional tool (same posture as the ctx7-absent fallback). Install to enable:uv tool install git+https://github.com/NVIDIA/skillspector.git. - Static only, no egress.
--no-llmis hard-coded by the wrapper (not merely documented), so no skill content is ever sent over the network and no API key is required — consistent with the “no silent network” posture above. - Gate policy. HIGH/CRITICAL
risk_severityblocks promotion (exit 1, findings’rule_id/messagesurfaced); MEDIUM warns but allows; LOW/NONE/not-scanned pass. - Baseline suppression. A per-skill baseline is stored at
~/.vibeflow/security-baselines/<name>.yaml— outside the skill’s own tree, so a re-import cannot wipe it and re-flag already-triaged findings.
This gate closes the trust boundary opened by the shared catalog: a skill discovered once and promoted becomes trusted for every project on the machine, so the promotion step is the right place to enforce automated review.
npm package risk model
npm packages are external executable dependencies, not trusted skills.
Safety checks:
- verify package name
- inspect repository and maintainer
- pin version
- prefer --ignore-scripts
- run in sandbox when possible
- avoid packages requesting credentials
- log install reason
Hook enforcement
Hooks must block only clearly unsafe actions. When uncertain, they should warn or require approval.
allow → normal action
warn → low/medium risk
require_approval → elevated risk
block → clearly unsafe or irreversible
Codex hook config is global, not per-repo
Codex’s native hook configuration is owned by Codex at ~/.codex/hooks.json, not by an
individual repository. vf hooks emit --yes merges VibeFlow’s PreToolUse and PostToolUse
entries there and enables [features] codex_hooks = true in ~/.codex/config.toml. This
affects every repository that uses Codex on that machine; VibeFlow warns before the write.
Codex’s native veto covers Bash/shell tool calls only. Edit, Write, apply_patch, and MCP calls are not intercepted by that hook, so VibeFlow retains its apply-time diff gate for Codex.
To revert, remove VibeFlow’s PreToolUse and PostToolUse entries from
~/.codex/hooks.json, then remove or set codex_hooks = false under [features] in
~/.codex/config.toml.
Review-thread gate (CI, read-only)
.github/workflows/review-thread-gate.yml is a read-only GitHub Actions gate that
blocks a merge while any review thread on the current PR head is unresolved. It honors
the least-privilege and no-code-execution posture:
- Job permissions are exactly
pull-requests: read; the top-levelpermissionsis{}. The only token used isGITHUB_TOKEN(no PAT, nopull_request_target). - It never runs
actions/checkoutand never executes PR code. It queries GitHub GraphQL for the PR’s liveheadRefOidand paginatedreviewThreads, and comparesheadRefOidagainst the eventpull_request.head.shaso a stale-queued success after a force-push fails closed. - Never printed: the token, full event payload, comment body, or GraphQL request
headers. Unresolved current threads are reported as
path:line — @author — url.
Pre-push review-evidence gate (local only)
The repository pre-push hook is deterministic local fast feedback. It binds verification
to the pushed current HEAD, reads only local commit evidence, and fails closed on missing,
stale, malformed, unreadable, or failed applicable evidence. It performs no network, LLM,
GitHub API, or Copilot call and never uploads/commits/attests local evidence. User-owned
hooks are preserved. git push --no-verify bypasses this local gate by Git design; the
separate required remote review-thread-gate remains authoritative.
Secrets handling
Agents and hooks must not print or store secrets.
Rules:
- never include tokens in prompts
- redact environment values
- block direct reads of .env unless explicitly approved
- do not store credentials in SKILL.md
- do not send secrets to external docs/skill services
Spawned engine env scrub
Every engine subprocess vf launches (claude, codex, copilot, …) used to inherit
the entire host environment. An agent run to fix a CSS bug would receive
AWS_SECRET_ACCESS_KEY, STRIPE_SECRET_KEY, database URLs, and any other secret in the
operator’s shell. VibeFlow now filters the env at both spawn sites
(src/dispatch/spawners.ts, src/commands/coord.ts) via a single filterEnv() helper —
no Docker, no new dependency.
Default policy (conservative — drop known secrets, pass the rest):
ALWAYS KEEP (never dropped, even in strict mode):
PATH HOME SHELL USER LOGNAME LANG TERM TMPDIR TMP TEMP PWD, LC_* prefix,
vf's own VF_* / VIBEFLOW_* (so the VF_DENY_TOOLS hint survives),
engine auth vars: ANTHROPIC_API_KEY OPENAI_API_KEY GH_TOKEN GITHUB_TOKEN GEMINI_API_KEY,
(Windows also: SYSTEMROOT PATHEXT COMSPEC APPDATA LOCALAPPDATA USERPROFILE)
DEFAULT DENY (globs; ALWAYS_KEEP overrides any match):
AWS_* AZURE_* GCP_* GOOGLE_APPLICATION_CREDENTIALS STRIPE_* TWILIO_* SLACK_*
SENTRY_* NPM_TOKEN DOCKER_* DATABASE_URL
*_SECRET *_SECRET_KEY *_PRIVATE_KEY *_PASSWORD *_TOKEN *_API_KEY
Note *_TOKEN / *_API_KEY are denied by default (secret-shaped) — the specific engine
auth vars above are in ALWAYS_KEEP, so they ride through while every other token/key is dropped.
Configuring the policy (per-repo, .vibeflow/SETTINGS.json, absent = default):
vf config env-policy status # print the effective policy + what WOULD be dropped now
vf config env-policy deny 'MY_APP_*' # add a glob to drop on top of the built-in denylist
vf config env-policy allow 'MY_*' # add a glob; a non-empty allow[] = STRICT pass-only mode
vf config env-policy reset # clear config, back to the conservative default
In strict mode (any allow glob set) ONLY ALWAYS_KEEP + allow-matching vars pass;
everything else is dropped. On Windows name matching is case-insensitive (Path == PATH).
Trusted namespace caveat:
VF_*/VIBEFLOW_*are treated as vf’s own signalling namespace and are kept unconditionally (soVF_DENY_TOOLSsurvives). Do not store an unrelated secret under aVF_/VIBEFLOW_name — the scrub will not drop it.
Audit: dropped variable NAMES (never values) are logged once per dispatch (the coord path
emits { kind: "coord-env-scrub", dropped } to vf logs), matching the tool deny-list audit pattern.
The read-only policy summary is surfaced in the web UI Settings panel (“Env scrub”).
Verify Docker sandbox
vf verify --sandbox docker is an opt-in boundary for synchronous CLI toolchain and waiver
gates. Host execution remains default. Sandbox preflight requires a running Docker daemon,
a locally available digest-pinned image, one supported lockfile, a dependency volume whose
vibeflow.lock-sha256 label exactly matches that lockfile, and a non-root host UID/GID.
Failure aborts; vf never silently retries on the host, pulls/builds an image, or installs
dependencies.
The container receives --network none, no -e/--env-file, no Docker socket or home
mount, --cap-drop ALL, no-new-privileges, PID/CPU/memory limits, and an explicit host
UID/GID. Exactly two mounts are supplied: a writable disposable source copy at /w and the
named dependency volume read-only at /w/node_modules. The active worktree and its .git metadata are not
mounted. The copy lives temporarily under .vibeflow/sandbox-* so Docker Desktop/Colima can
bind-mount it on macOS. A bounded gate timeout triggers best-effort docker rm -f; the copy
is removed after success or failure.
Residual trust remains: the operator-supplied image, dependency volume, Docker daemon, and container-runtime isolation. Code can mutate its disposable copy. v1 does not sandbox the web verify API, orchestrate per-unit gates, or arbitrary acceptance commands.
Local web server
The vf / vf ui server is the interactive console. Its default surface is AI-first
Home (session rail + central conversation pane). Repository intake is the TTY questionnaire
in vf init, not a vf ui mode. Because Home exposes write actions, it is hardened as follows
(implemented in src/server.ts):
- defaults to 127.0.0.1; every non-loopback `--host` intentionally exposes the server
- on a non-loopback bind, the returned owner URL contains a single-use bootstrap capability.
The first exact request consumes it, installs a process-local `HttpOnly`, `SameSite=Strict`
LAN page-session cookie, and redirects to `/` so the capability leaves the address bar.
Unauthenticated root loads return `401`; the bootstrap cannot be replayed and is never
persisted or included in the normal server banner, logs, or errors. `--no-open` deliberately
prints the owner-only bootstrap URL once so a human can open it without browser automation
- only a page with that LAN page-session cookie receives the per-process CSRF token.
Legacy `fetch`/API requests require it in the `x-vibeflow-token` header; Host headers are
not treated as authentication, and the CSRF token is not a Conversation Home credential
- browser `EventSource` cannot set custom headers, so it supplies that same page token
in the canonical `token` query parameter for protected non-conversation SSE;
header-capable API clients continue to use the header
- conversation sessions are auto-issued only on loopback. A normal LAN page receives no
vf_conversation_session cookie, so conversation JSON, artifact, and stream-token routes
fail closed with 401; the page CSRF token is not a conversation credential
- static assets are network-reachable; GET `/` and `/index.html` require LAN page authority,
and sensitive read routes remain CSRF-guarded
- mutations are admitted only by the server's explicit method/path allowlist. It covers workflow,
settings, attachment, plan-review, skill, hook-approval, and conversation/action namespaces;
an unlisted write path is rejected rather than falling through to a generic handler
- per-process CSRF token: embedded only in an authorized page; protected `fetch`/API requests
use the `x-vibeflow-token` header, while protected non-conversation `EventSource` streams
(including deprecated `/events`) use the narrow `token` query exception described above
- loopback mode uses the exact Host allowlist (127.0.0.1 / localhost / ::1) and requires any
Origin/Referer to be loopback. LAN mode does not treat Host as authority: protected routes
require the page CSRF capability regardless of Host
- hook CLI approval on a LAN bind uses a second OS-selected listener bound only to 127.0.0.1.
`.vibeflow/.ui-port` stores its credential-free `hook_origin` plus process/port metadata—never
the LAN page token or bootstrap. The hook client accepts only exact loopback HTTP origins; the
bridge rejects browser Origin/Referer requests, requires JSON, and caps pending bodies at 64 KiB
- JSON body capped (64 KB); uploads streamed to disk and capped (50 MB/file), partial files
removed on overflow; malformed or oversized bodies are rejected
- attachment filenames are reduced to a single safe path segment (basename; no separators,
traversal, control/null bytes, dotfiles, or over-long names) and confined to
<repo>/.vibeflow/attachments/ — verified by a resolve()/startsWith() check
- no remote scripts: the page ships zero third-party JS, so a compromised CDN cannot
reach the same-origin write API (Content-Security-Policy restricts to 'self')
- user input is never used as a filesystem path; canonical writes target fixed .vibeflow/*
paths and engine names validated against the ENGINES allowlist. The repo path the user
picks is resolved to an existing directory; writes to it require the per-process token
- web-initiated init never shells out to $VIBEFLOW_AI (useAi:false); the CLI keeps the AI bridge enabled
Private One-Shot File Context
The home composer can stage a private file range for a draft or message. That private
context is captured once per scope, attached to the next turn, and then cleared or
discarded after use or replacement. The browser keeps only the selection authority;
the runtime owns the actual turn attachment so the private range does not silently
carry over into an unrelated conversation. Its transport is canonical JSON prefixed by
VF-PRIVATE-FILE-RANGES/1, separate from the public VF-TURN/1 envelope. It never becomes
public trace/browser persistence.
Exact by-id resume is limited to Claude, Codex, and OpenCode. On valid exact resume, the
selected CLI keeps its own private session history. VibeFlow delivers only new applicable
user messages and peer-agent responses/reactions; it does not replay the recipient’s own
prior output. Without valid exact authority, turn delivery includes applicable public context
plus up to eight of the recipient’s own public responses, each capped at 2 KiB UTF-8 with
source digest, provenance, and count/truncation metadata. Copilot and Antigravity therefore
cannot silently claim exact resume or omit own context. A large Copilot
.vibeflow/dispatch/<unit>.md prompt file is only
a transport fallback addressed by a short argv pointer, not session memory.
Owned Process Identity and Containment
Every canonical async CLI launch records the supervisor PID, CLI PID, host,
attempt/operation, and exact process-start identity. PID alone is never ownership proof.
Terminal release requires process exit/quiescence plus streams-drained for stdout and
stderr. Recovery follows the same fail-closed identity contract:
| Platform | Owned scope | Proof strength | Boundary |
|---|---|---|---|
| Windows | windows-job | kernel-contained | Kill-on-close Job Object established before receipt/spawn; creation ticks are queried through PowerShell/CIM without /bin/ps. |
| Linux | posix-process-group | cooperative-lineage | Isolated process group plus boot id and /proc start ticks. |
| macOS | posix-process-group | cooperative-lineage | Isolated process group plus exact Darwin libproc seconds/microseconds. |
The POSIX boundary is cooperative because a descendant can deliberately leave its process
group. vf doctor --fix may recover only an exact proved dead or identity-mismatched owner;
live and identity-unprovable owners remain quarantined for manual inspection. The Windows contract
has injected platform regression coverage. Live Windows evidence is accepted only from a green,
exact-SHA windows-latest CI smoke job; a local macOS/Linux run is not a Windows canary.
Audit log
Every run should log:
- user approvals
- commands run
- files read/written
- skills used
- external sources accessed
- hook decisions
- engine selected
- final verification result
Audit logs should avoid storing secrets or full sensitive file contents.
Related: Architecture · Hooks and Guardrails Edit this page on GitHub