Security Model
Contents
- Core Principle
- Default Safety Posture
- Permission Classes
- Approval Required Actions
- Protected Paths
- External Skill Trust Model
- Shared Catalog Trust Boundary
- External Skill Security Scan
- npm Package Risk Model
- Hook Enforcement
- Secrets Handling
- Local Web Server
- 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.
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.
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.
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)
Pre-push is deterministic local fast feedback. It binds verification to pushed current
HEAD, reads only local 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 transports local evidence. User-owned hooks are preserved.
git push --no-verify bypasses local feedback; remote review-thread-gate remains authority.
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”).
Local web server
The vf ui server is the interactive console (intake → generate → dispatch). Because it now
exposes write actions, it is hardened as follows (implemented in src/server.ts):
- binds 127.0.0.1 only — never 0.0.0.0, never a public interface
- GET /, /state, /events are read-only (dashboard + live ledger)
- writes only via POST /api/init, /api/dispatch, /api/detect, /api/units, and POST/DELETE
/api/upload (binary attachments); GET /, /state, /events, /api/attachments are read-only
- per-process CSRF token: embedded in the page, required in the x-vibeflow-token header
- exact-match Host allowlist (127.0.0.1 / localhost / ::1) — mitigates DNS rebinding
- Origin/Referer, when present, must be loopback
- 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); only the CLI may
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