# PROP-020 — Install hooks {#root}

@status:impl/done

[p01] @fact:status-line **Status: 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](../vibe-registry/PROP-021-submodule-sources.xml) submodule sources,
[PROP-022](PROP-022-materialization-modes.xml) materialization modes,
[PROP-023](../vibe-registry/PROP-023-bridge-packages.xml) bridge packages). The
four compose to solve bridge packages but each stands alone — hooks exist for
*any* package, not only bridges. @status:impl/done

[p02] @fact:related **Related:** [PROP-009](PROP-009-loading-model.xml) (the install/materialise
pipeline hooks slot into), [PROP-007](PROP-007-workspace.xml) (workspace +
`vibedeps/`), [PROP-022](PROP-022-materialization-modes.xml) (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](../vibe-mcp/PROP-015-mcp-integration.xml#skill)
(skill projection reads the slot a hook prepared), [PROP-019 §2.13](../../common/PROP-019-version-manager.xml#security)
(the same "a build/script the user installs is code they chose to run" trust
posture), [PROP-000 §20](../../common/PROP-000.xml#token-secrecy) (the publish
token a hook never sees). @status:spec/done

## 1. Motivation {#motivation}

### 1.1 The problem — install is pure file I/O, with no preparation step {#problem}

- [p03] @fact:install-pure-io Today `vibe install` resolves, fetches, materialises a package's tree into
  `vibedeps/`, regenerates boot artefacts, and writes the lockfile — all pure
  file copying. @status:impl/done
- @fact:no-preparation-step 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. @status:impl/done

- [p04] @fact:forcing-case-bridges The forcing case is bridge packages ([PROP-023](../vibe-registry/PROP-023-bridge-packages.xml)):
  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. @status:spec/done
- @fact:hooks-universal But the need is general — any package may want a
  post-materialise step — so hooks are a **universal** mechanism, not a
  bridge-only feature. @status:spec/done

### 1.2 What this is — declared lifecycle scripts, run per package {#what}

- [p05] @fact:WHAT-HOOKS-ARE A package may declare `pre-install` / `post-install` scripts 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. @status:impl/done
- @fact:EFFECTS-EPHEMERAL Only a hook's edits
  to materialiser-owned recorded payload are ephemeral: reinstall, update or
  integrity repair restores those bytes per
  [PROP-022](PROP-022-materialization-modes.xml), then reruns hooks exactly
  when that payload diff is nonempty. Hook-created unrecorded state is outside
  `.vibe-slot.toml` ownership and survives by design; a rerun may compound it,
  so hooks must be idempotent until a separate hook-output ownership contract
  exists. @status:impl/done

## 2. Decisions {#decisions}

### 2.1 Two phases, anchored to the materialise pipeline {#phases}

[p06] @fact:req-phases `req r1` @status:impl/done

[p07] @fact:ONE-PER-PHASE A package declares at most one script per phase: @status:impl/done

- [p08] @fact:PHASE-PRE-INSTALL **`pre-install`** — runs immediately after the package's slot is fully
  populated (content materialised, submodules fetched per
  [PROP-021](../vibe-registry/PROP-021-submodule-sources.xml)) and **before**
  vibevm uses the slot (before boot regeneration, before any later
  `vibe skill` projection reads it). This is the "bring the tree into order"
  hook. @status:impl/done
- @fact:PHASE-POST-INSTALL **`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. @status:impl/done

[p09] @fact:CWD-IS-SLOT The hook's **working directory is the package's materialised slot**; it sees
exactly the tree vibevm will use. @status:impl/done

- [p10] @fact:RESET-THEN-RERUN **On 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-place` remains a separate git-native reset whose `git clean -dfx` removes untracked hook output. The landed `cli_hook_rerun::reinstall_runs_post_hook_once_only_for_a_nonempty_force_diff` proof records the copy-slot boundary directly: `.hook-count` is `1` after install, remains `1` after empty reinstall, and becomes `2` after a recorded-payload repair. Copy/hardlink hooks therefore remain responsible for idempotence until a separate hook-output ownership contract exists. @status:impl/done
- @fact:RESET-IS-MODE-PROPERTY Reset remains a
  materialization-mode property: `copy`/`hardlink` restore only the recorded
  footprint and preserve unrecorded outputs, while `in-place` reports its
  git-native change. Hooks consume that exact change report to decide whether
  the one rerun occurs. @status:impl/done

### 2.2 Interpreter selection is OS-derived {#script-selection}

[p11] @fact:req-interpreter `req r1` @status:impl/done

[p12] @fact:SCRIPT-FORMS A package ships a phase script as `<base>.sh` (portable, POSIX shell) and/or
`<base>.ps1` (PowerShell). The runner picks per host: @status:impl/done

- [p13] @fact:UNIX-SELECTION **Unix (macOS / Linux):** run `<base>.sh` via `bash`. A `.ps1` is ignored. @status:impl/done
- @fact:WINDOWS-SELECTION **Windows:** prefer `<base>.sh` via **Git Bash** when a `bash` is found
  (one cross-platform script for `.sh` packages); else fall back to
  `<base>.ps1` via **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. @status:impl/done

- [p14] @fact:HOOK-ENV The 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](../../common/PROP-024-code-bearing-packages.xml#build) adds
  `VIBE_PROJECT_ROOT`, the workspace absolute root, so a build hook can target a
  gitignored build dir *outside* the slot; it lands with that work.) @status:impl/done
- @fact:TOKEN-NEVER-IN-ENV The publish token
  ([PROP-000 §20](../../common/PROP-000.xml#token-secrecy)) is **never** placed in
  a hook's environment. @status:impl/done

[p15] @fact:RUNNER-SEAM The process runner is an injectable seam (`HookRunner`) so tests assert the
selection logic and argument/env shape without spawning real processes. @status:impl/done

### 2.3 Trust posture — installation is consent, observability is the gate {#trust-gate}

[p16] @fact:req-trust `req r1` @status:impl/done

[p17] @fact:TRUST-CHEAP **Historical 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. @status:spec/done

- [p18] @fact:ALLOW-LIST **Superseded design: allow-listed groups.** A config key (global
  `~/.vibe/config.toml` `[hooks].allowed_groups`, with a project-level
  override) lists trusted package groups. **`org.vibevm` is 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. @status:spec/done
- @fact:FIRST-RUN-CONSENT **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). @status:spec/done
- @fact:NON-INTERACTIVE-SAFETY **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-hooks` opt-in. A script must never execute
  unseen third-party code by default. @status:spec/done

[p19] @fact:INSTALLATION-CONSENT-SUCCESSOR **Current 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. @status:impl/done

### 2.4 Hooks are declared in the manifest {#manifest}

[p20] @fact:req-manifest-hooks `req r1` @status:impl/done

[p21] @fact:HOOKS-TABLE Hooks live in a package-role `[hooks]` table in `vibe.toml`: @status:impl/done

[p22]
```toml
[hooks]
pre-install  = "hooks/prepare"   # base path, relative to package root
post-install = "hooks/finalise"
```

- [p23] @fact:BASE-PATH-VALUE The value is a **base path without extension**; the runner resolves `.sh` /
  `.ps1` beside it per §2.2. @status:impl/done
- @fact:PACKAGE-ONLY-TABLE The table is package-only (its presence on a
  `[project]`-role manifest is a validation error, like the other package-only
  sections). @status:impl/done
- @fact:ABSENT-DEFAULT An empty/absent `[hooks]` means no hooks — the common case. @status:impl/done

### 2.5 Failure semantics are phase-specific {#failure}

[p24] @fact:req-failure `req r1` @status:impl/done

[p25] @fact:STREAMS-AND-EXIT A hook's stdout/stderr stream to the user. A non-zero exit is handled by
phase: @status:impl/done

- [p26] @fact:FAIL-PRE-ABORTS **`pre-install` failure → 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. @status:impl/done
- @fact:FAIL-POST-FLAGS **`post-install` failure → 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. @status:impl/done

## 3. Rejected alternatives {#rejected}

- [p27] @fact:REJ-INLINE-COMMANDS **Inline `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. @status:spec/done
- @fact:REJ-BOTH-EXTENSIONS **Running every matching extension on Windows** (`.sh` *and* `.ps1`) —
  rejected: one logical hook per phase keeps behaviour predictable; the
  selection is a single deterministic choice (§2.2). @status:spec/done
- @fact:REJ-GENERAL-LIFECYCLE **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. @status:spec/done

## 4. Out of scope {#out-of-scope}

- [p28] @fact:OOS-CONTENT-SCANNING **Content 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. @status:spec/done
- @fact:OOS-SANDBOXING **Sandboxing / capability-limiting** hooks (containers, seccomp). Hooks run
  with the user's privileges, like `cargo build` scripts or `npm postinstall`. @status:spec/done
- @fact:OOS-NON-GIT-RESET **Non-git slot reset for `in-place`** — a hook over an `in-place`
  ([PROP-022 §2.4](PROP-022-materialization-modes.xml#in-place)) package whose
  source is not git has no cheap reset; `in-place` therefore requires a git
  source (PROP-022 §4). @status:spec/done

## 5. Acceptance {#acceptance}

- [p29] @fact:ACC-PHASE-TIMING A package with `[hooks].pre-install` runs the script in its slot before boot
  regeneration; `post-install` runs after the lockfile is written. @status:impl/done
- @fact:ACC-INTERPRETER Interpreter selection follows §2.2 on each OS; a declared hook with no usable
  interpreter errors rather than silently skipping. @status:impl/done
- @fact:ACC-CONSENT 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. @status:impl/done
- @fact:ACC-FAILURE-SEMANTICS A `pre-install` non-zero exit rolls back the slot and fails the package; a
  `post-install` failure installs-but-flags. @status:impl/done
- @fact:ACC-RESET-RERUN 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. @status:impl/done
- @fact:ACC-RUNNER-SEAM The `HookRunner` seam lets tests assert selection/env/failure paths without
  spawning real processes. @status:impl/done
- @fact:ACC-FLOOR-GREEN Full `self-check.sh` green; conform 0/0/0; specmap clean. @status:impl/done

