PROP-020 — Install hooks
01Status: IMPLEMENTED (specified 2026-06-24 in an owner-requested design
session; verified against the tree 2026-07-25 by the spec-actualization
campaign). [hooks] parses in vibe-core (manifest/package/hooks.rs),
vibe-workspace/src/hooks.rs runs the whole contract — the pre/post phases, the
Git-Bash-first Windows interpreter selection, the trust gate over
DEFAULT_ALLOWED_GROUPS, the ran / skipped / failed statuses and the
VIBE_HOOK_* environment — and vibe-install's apply pipeline drives it.
R1 successor 4503fdb6/9c545f0d makes the exact nonempty materialisation
report the sole hook-rerun trigger, including verify repair. One of
four orthogonal specs carved from the bridge-packages design (the others:
PROP-021 submodule sources,
PROP-022 materialization modes,
PROP-023 bridge packages). The
four compose to solve bridge packages but each stands alone — hooks exist for
any package, not only bridges.
02Related: PROP-009 (the install/materialise
pipeline hooks slot into), PROP-007 (workspace +
vibedeps/), PROP-022 (a hook's
working tree is the materialised slot; how its edits are reset on update is a
materialization-mode property), PROP-015 §2.6
(skill projection reads the slot a hook prepared), PROP-019 §2.13
(the same "a build/script the user installs is code they chose to run" trust
posture), PROP-000 §20 (the publish
token a hook never sees).
1. Motivation
1.1 The problem — install is pure file I/O, with no preparation step
- 03Today
vibe installresolves, fetches, materialises a package's tree intovibedeps/, regenerates boot artefacts, and writes the lockfile — all pure file copying. - A package that needs a preparation step after its content lands (normalise a vendored layout, generate a derived file, assemble a clean skill subtree out of an upstream repo's mess) has nowhere to put it.
- 04The forcing case is bridge packages (PROP-023): a maintainer wraps someone else's repository whose structure does not match vibevm conventions, and needs to bring it into order before vibevm's skill machinery reads it.
- But the need is general — any package may want a post-materialise step — so hooks are a universal mechanism, not a bridge-only feature.
1.2 What this is — declared lifecycle scripts, run per package
- 05A package may declare
pre-install/post-installscripts in its manifest. vibevm runs them at fixed points in the install pipeline, in the package's own materialised slot, choosing the right interpreter for the host OS. - Only a hook's edits
to materialiser-owned recorded payload are ephemeral: reinstall, update or
integrity repair restores those bytes per
PROP-022, then reruns hooks exactly
when that payload diff is nonempty. Hook-created unrecorded state is outside
.vibe-slot.tomlownership and survives by design; a rerun may compound it, so hooks must be idempotent until a separate hook-output ownership contract exists.
2. Decisions
2.1 Two phases, anchored to the materialise pipeline
06req r1
07A package declares at most one script per phase:
- 08
pre-install— runs immediately after the package's slot is fully populated (content materialised, submodules fetched per PROP-021) and before vibevm uses the slot (before boot regeneration, before any latervibe skillprojection reads it). This is the "bring the tree into order" hook. post-install— runs after the install run is durable for that package (lockfile written, boot artefacts regenerated). For finalisation that needs the package already registered.
09The hook's working directory is the package's materialised slot; it sees exactly the tree vibevm will use.
- 10On update, reinstall or integrity repair, the materialiser restores its recorded payload, then hooks rerun if and only if that payload diff is nonempty. For
copy/hardlink, restoration is the PROP-054 §9.3 record diff: an unrecorded hook-created path survives and may be changed again by the rerun.in-placeremains a separate git-native reset whosegit clean -dfxremoves untracked hook output. The landedcli_hook_rerun::reinstall_runs_post_hook_once_only_for_a_nonempty_force_diffproof records the copy-slot boundary directly:.hook-countis1after install, remains1after empty reinstall, and becomes2after a recorded-payload repair. Copy/hardlink hooks therefore remain responsible for idempotence until a separate hook-output ownership contract exists. - Reset remains a
materialization-mode property:
copy/hardlinkrestore only the recorded footprint and preserve unrecorded outputs, whilein-placereports its git-native change. Hooks consume that exact change report to decide whether the one rerun occurs.
2.2 Interpreter selection is OS-derived
11req r1
12A package ships a phase script as <base>.sh (portable, POSIX shell) and/or
<base>.ps1 (PowerShell). The runner picks per host:
- 13Unix (macOS / Linux): run
<base>.shviabash. A.ps1is ignored. - Windows: prefer
<base>.shvia Git Bash when abashis found (one cross-platform script for.shpackages); else fall back to<base>.ps1via PowerShell when one is found. A phase that declares a script but finds no usable interpreter is a hard error with a remediation hint — never a silent skip.
- 14The runner passes a documented environment:
VIBE_PACKAGE_GROUP,VIBE_PACKAGE_NAME,VIBE_PACKAGE_VERSION,VIBE_PACKAGE_KIND,VIBE_PACKAGE_DIR(the slot, also CWD),VIBE_HOOK_PHASE. (PROP-024 §2.3 addsVIBE_PROJECT_ROOT, the workspace absolute root, so a build hook can target a gitignored build dir outside the slot; it lands with that work.) - The publish token (PROP-000 §20) is never placed in a hook's environment.
15The process runner is an injectable seam (HookRunner) so tests assert the
selection logic and argument/env shape without spawning real processes.
2.3 Trust posture — installation is consent, observability is the gate
16req r1
17Historical proposal, superseded. Running a package's hook is running
third-party code at install time. This section originally proposed an
allow-list and per-run consent; PROP-054 ##INSTALL-IS-CONSENT later replaced
that permission model for every extension handler.
- 18Superseded design: allow-listed groups. A config key (global
~/.vibe/config.toml[hooks].allowed_groups, with a project-level override) lists trusted package groups.org.vibevmis in the allow-list by default. A package whose group is allow-listed runs its hooks with no prompt. This key was never a product input and is not part of the current configuration grammar. - Superseded design: first-run consent. On the first hook run of a non-allow-listed
package, vibevm prints what will run (phase, script path, group) and asks
y/n. Declining skips the hook and marks the package install as hooks-skipped (surfaced, not silent). - Superseded design: non-interactive abort. With
--assume-yes/ in CI, allow-listed packages still run; a non-allow-listed package's hook is not run silently — the install aborts with a hint to either allow-list the group or pass an explicit--allow-hooksopt-in. A script must never execute unseen third-party code by default.
19Current law. Installing the package is the consent to run its declared
hooks. There is no [hooks].allowed_groups, first-run prompt, or
--allow-hooks permission layer. Safety is the PROP-054 observability model:
the manifest is statically inspectable, selected contributions are narrated,
and durable run evidence identifies what ran and which package supplied it.
2.4 Hooks are declared in the manifest
20req r1
21Hooks live in a package-role [hooks] table in vibe.toml:
22[hooks]
pre-install = "hooks/prepare" # base path, relative to package root
post-install = "hooks/finalise"
- 23The value is a base path without extension; the runner resolves
.sh/.ps1beside it per §2.2. - The table is package-only (its presence on a
[project]-role manifest is a validation error, like the other package-only sections). - An empty/absent
[hooks]means no hooks — the common case.
2.5 Failure semantics are phase-specific
24req r1
25A hook's stdout/stderr stream to the user. A non-zero exit is handled by phase:
- 26
pre-installfailure → the package install aborts. The slot is rolled back (removed) and the install reports the failing package; vibevm never registers or projects from a package whose preparation failed. post-installfailure → the package is installed but flagged. The package is already durable (lockfile written); the failure surfaces as a warning with the captured output, never a silent success.
3. Rejected alternatives
- 27Inline
command = "..."strings in the manifest instead of files — rejected: a versioned script file is auditable, diffable, and platform-split (.sh/.ps1); an inline string hides the code in TOML and resists review. - Running every matching extension on Windows (
.shand.ps1) — rejected: one logical hook per phase keeps behaviour predictable; the selection is a single deterministic choice (§2.2). - A general lifecycle (
pre-uninstall,pre-build, …) now — deferred: only the two phases the bridge case needs are specified; more can be added later behind their own anchors without disturbing these.
4. Out of scope
- 28Content scanning / the LLM "antivirus". A future gate that inspects a package's hooks (and code) for malicious behaviour is far-backlog. Until it lands, hook execution is an explicitly accepted risk governed by dependency selection plus PROP-054 observability. This is the deliberate posture, not an oversight — recorded here as the project's stance.
- Sandboxing / capability-limiting hooks (containers, seccomp). Hooks run
with the user's privileges, like
cargo buildscripts ornpm postinstall. - Non-git slot reset for
in-place— a hook over anin-place(PROP-022 §2.4) package whose source is not git has no cheap reset;in-placetherefore requires a git source (PROP-022 §4).
5. Acceptance
- 29A package with
[hooks].pre-installruns the script in its slot before boot regeneration;post-installruns after the lockfile is written. - Interpreter selection follows §2.2 on each OS; a declared hook with no usable interpreter errors rather than silently skipping.
- Any installed package's declared hook is eligible without a second consent prompt or group allow-list; the execution plan and resulting run evidence identify the hook, phase and providing package.
- A
pre-installnon-zero exit rolls back the slot and fails the package; apost-installfailure installs-but-flags. - Updating, reinstalling or integrity-repairing a package restores the prior hook's recorded payload edits (per the materialization mode); a nonempty payload change reruns hooks exactly once and an empty change runs none.
- The
HookRunnerseam lets tests assert selection/env/failure paths without spawning real processes. - Full
self-check.shgreen; conform 0/0/0; specmap clean.