Security model
Vibe Factory lets language models edit files and run commands in a developer’s repository. The security model answers one question: what is the worst a confused or manipulated model can do, and how do we bound it? It is built in layers; each layer is independent and any one of them can be tightened without touching the others.
┌───────────────────────────────────────────────────────────┐
│ 5. Isolation git worktree (default) / plugin sandbox │
│ 4. Hooks before_tool veto, audit, prompt rules │
│ 3. Shell policy parser + blocked programs + validators │
│ 2. Permissions read / write / execute / network │
│ 1. Path containment every path inside the workspace │
└───────────────────────────────────────────────────────────┘
1. Path containment (vibe-core)
ToolContext::resolve_path is the single entry point for paths. It joins relative paths
to the workspace root, normalises . and .. lexically, canonicalises the longest
existing prefix (resolving symbolic links), re-appends the non-existing suffix and checks
that the result starts with the canonical workspace root (or one of
Permissions::extra_read_paths).
Two subtleties are handled explicitly:
- a dangling symbolic link cannot be canonicalised; the path is denied, because its eventual target is unknown and a write would create a file at that target;
- the writing tools (
write_file,edit_file, thebashworking directory) walk every component withsymlink_metadataand refuse any symbolic link, even one that points inside the workspace, so that a later retarget cannot redirect a write.
extra_read_paths are honoured for reading only; the writing tools require the canonical
path to be inside the workspace root proper.
2. Permissions (vibe-core)
Permissions { read, write, execute, network, extra_read_paths } travel with every
ToolContext. The built-in tools check them before doing anything. Presets:
read_only(), local() (default: everything but network) and all(). The pipeline
derives them from SecurityConfig and from the agent’s ToolSelection: a read-only agent
never receives a writing tool in the first place, and even if it did, the permission check
would refuse.
3. Shell policy (vibe-tools)
The bash tool hands the command line to SecurityPolicy::validate_with_network before
spawning anything. The policy is a denylist with per-command validators, optionally
narrowed by an allowlist (ADR-004).
Parsing
command_parser::parse_command splits on |, ||, &&, ;, & and newlines, descends
into $(…), backticks and ( … ), strips env/time/nice/nohup/xargs/timeout/
exec/command/builtin wrappers and VAR=value prefixes, and returns one
CommandSegment { program, argv } per command, up to MAX_NESTING_DEPTH (16). It fails
closed on case/function constructs, env -S, here-documents containing substitutions,
redirects without target and unbalanced quotes or parentheses. Program names that would be
computed at run time ($, backtick, *, ?, [, {, }, ^, %) are denied.
Rules, in order
BLOCKED_PROGRAMSandSecurityConfig::blocked_commands→ deny.SecurityConfig::allowed_commandsnon-empty and program not in it (nor inALWAYS_ALLOWED, the harmless builtins) → deny.NETWORK_PROGRAMSand network not permitted → deny.- Per-command validators:
rm,chmod,git,kill,pkill/killall, the shells (-cvalidated recursively; stdin-fed shells denied),eval,find -exec, database clients,dropdb/dropuser. The exact rules are listed in the user guide.
Every denial is a sentence returned to the model as an error tool result
(ToolOutput::error, metadata.denied = true), never a framework error, so the agent can
adapt instead of crashing the run.
Execution
Commands run through sh -c (Unix) or cmd /C (Windows) in the workspace, with stdin
closed, GIT_TERMINAL_PROMPT=0 and pagers disabled. A timeout kills the whole process
group (taskkill /T /F on Windows). Output is merged, capped at 30 000 characters
keeping head and tail, and ends with the exit code.
4. Hooks (vibe-core, plugins)
Hook::before_tool runs before every tool call with the ToolContext, tool name and
input; an Abort(reason) becomes an error tool result. after_tool sees every output.
augment_prompt injects project rules into the system prompt. Remote hooks (plugins)
fail closed: if the plugin cannot answer, the call is aborted. This is where teams put
policies the static layer cannot express.
5. Isolation (vibe-workspace, plugins)
The default workspace is a git worktree on its own branch (ADR-003); the project checkout is never modified until the merge step, which refuses to run on a dirty tree. With the git worktree workspace each subtask attempt also gets its own worktree, integrated into the task branch one at a time.
The container workspace (vibe_workspace::ContainerWorkspace) keeps the worktrees and
changes where commands run. vibe_core::CommandRunner is the seam: the bash tool checks
the policy and the paths as usual, then asks the runner for the process to spawn
(PreparedCommand: program, arguments, a cleanup command and the exit status that means
“the runner failed”). ContainerRunner returns docker run --rm (or podman run --rm)
with fixed hardening flags (--cap-drop=ALL, --security-opt=no-new-privileges,
--read-only, a /tmp tmpfs, --pids-limit), the worktree bind-mounted at /workspace,
its .git pointer mounted read-only over itself so a command cannot redirect the host’s git
to a crafted directory, and --network=none unless a network is both configured and
granted. The cleanup (rm --force <name>) runs when the command ends, times out or its
future is dropped, which kills everything the command started. Settings are validated
before any container starts (ContainerSettings::from_config): namespace-sharing networks,
sockets, writable mounts over .git or .vibe and reserved targets are refused, and there
is no raw flag passthrough. File tools are unaffected: they act on the host worktree under
path containment. VMs and remote sandboxes remain WorkspaceProvider plugins.
A worktree shares .git/config with the user’s checkout, so the provider treats the
repository configuration as untrusted after agents have run:
- every framework git command passes
NEUTRAL_CONFIG(core.hooksPathpointing at an empty directory,core.fsmonitor=false,core.pager=cat,core.editor=true,core.sshCommand=ssh) andGIT_CONFIG_NOSYSTEM=1, so no hook or helper runs during a checkpoint commit, a merge or a checkout; config_guard::ConfigSnapshotcapturesgit config --local --listatopen;mergediffs the current configuration against it and fails with the list of changed keys unless only volatile keys changed (branch.vibe/*,core.repositoryformatversion). A missing snapshot fails closed.accept_config_changesre-baselines after a human review.
The writing tools and the shell policy additionally refuse any path inside a .git
directory, so hook scripts cannot be planted without going through the configuration.
Secrets
Provider layers scrub sk-…, Bearer …, token=, api_key= and access_token= from
every logged message (vibe_providers::scrub_secrets). API keys are read from environment
variables by default (api_key_env); inline keys in config.toml are supported but
discouraged. Plugins receive only the environment you declare in their configuration.
Known limits
- The shell policy is defence in depth, not a sandbox: interpreters (
python -c,node -e,perl -e) and build tools that run arbitrary scripts are allowed. - On Windows the command is parsed with POSIX rules before being handed to
cmd /C;cmd-specific tricks beyond^and%escapes may not be recognised. - Prompt injection through repository content (a README telling the agent to run something) is mitigated by the same layers, not by detection.
Report weaknesses privately to the maintainers before opening a public issue.