# How Project-Defined Hooks Are Gated and Approved in Worktrunk

> Learn how Worktrunk gates and approves project-defined hooks through a two-step mechanism involving HookPlans and explicit approval before freezing into an immutable ApprovedHookPlan.

- Repository: [Maximilian Roos/worktrunk](https://github.com/max-sixty/worktrunk)
- Tags: how-to-guide
- Published: 2026-09-14

---

**Worktrunk enforces a strict two-step gating mechanism where project-defined hooks must be discovered, assembled into a mutable HookPlan, and explicitly approved—either interactively via HookPlan::approve or silently via HookPlan::approve_readonly—before being frozen into an immutable ApprovedHookPlan that execution handlers consume.**

Worktrunk treats every project-defined hook as potentially untrusted code that requires explicit user consent before execution. Because hooks are defined in repository configuration files and may contain arbitrary shell commands, the system prevents accidental or malicious execution through a mandatory approval gate. This architectural constraint ensures that users always review—or explicitly opt into—the exact set of hooks that will run during operations like `wt merge`, `wt remove`, or `wt switch`.

## Hook Discovery and the Planning Phase

Before any hook executes, Worktrunk builds a structured execution plan using **HookPlanBuilder**. This component parses the project configuration and assembles a **HookPlan** describing which hooks could run and with what arguments.

In [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs) (lines 90–110), the builder initializes with project and user configuration, then provides methods to add specific hook phases:

```rust
let mut builder = HookPlanBuilder::new(project_cfg.as_ref(), user_cfg, pid);
let hook_plan = builder
    .add_pre_merge_hooks()?
    .add_post_merge_hooks()?
    .finish(); // Returns HookPlan (still mutable)

```

The resulting **HookPlan** remains mutable at this stage, allowing commands to accumulate multiple hook phases (pre-merge, post-merge, etc.) before presenting the complete set to the user. This separation between planning and approval ensures that the system can display the full execution context before requesting consent.

## The Approval Gate

Once the plan is complete, Worktrunk requires explicit authorization before converting the mutable plan into an executable contract. The **HookPlan** struct provides two approval pathways in [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs) (lines 298–327), both returning an **ApprovedHookPlan**—an immutable snapshot that freezes the hook list.

### Interactive Approval for CLI Workflows

For standard command-line operations, **HookPlan::approve** presents the user with a detailed prompt listing pending hooks and their arguments. The user must confirm execution, or pass `--yes` to skip the prompt programmatically:

```rust
// Interactive approval – blocks for user confirmation
let approved = hook_plan.approve()?; // Returns ApprovedHookPlan

```

This method ensures that users explicitly acknowledge each hook that will execute, preventing silent execution of newly introduced or modified project hooks.

### Silent Approval for Streaming UI

When hooks are required in contexts that cannot block for user interaction—such as the picker UI that streams worktree rows in real-time—Worktrunk uses **HookPlan::approve_readonly**. This method automatically approves the plan without prompting, creating the same immutable **ApprovedHookPlan** structure:

```rust
// Read-only approval – no user prompt, used by picker UI
let approved = hook_plan.approve_readonly();

```

This pattern appears in [`src/commands/worktree/switch.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/worktree/switch.rs) (lines 1577–1615), where the UI must maintain responsiveness while still respecting the gating architecture. The resulting **ApprovedHookPlan** guarantees that even silently-approved hooks are strictly limited to the pre-discovered set.

## Enforcing Execution Boundaries

After approval, the **ApprovedHookPlan** acts as an execution contract that cannot be modified. Command implementations in [`src/commands/merge.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/merge.rs) (lines 80–110), [`src/commands/remove.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/remove.rs) (lines 312–345), and [`src/commands/worktree/switch.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/worktree/switch.rs) receive the immutable plan and pass it to execution handlers.

The system enforces this boundary by requiring **execute_planned_hook** and **register_planned** functions to accept only `&ApprovedHookPlan` references, as implemented in [`src/output/handlers.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/output/handlers.rs). This type-level constraint prevents command implementations from adding unapproved hooks after the gate:

```rust
// Executor consumes only the immutable approved plan
execute_planned_hook(&approved, &repo, &worktree)?;

```

Because **ApprovedHookPlan** is created once and passed immutably, any subsequent changes to the project configuration have no effect on the current command execution. This design guarantees that **project-defined hooks are always gated behind explicit approval** and cannot silently expand their scope during operation.

## Summary

- **HookPlanBuilder** assembles mutable hook plans in [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs) (lines 90–110) before any user interaction occurs.
- **HookPlan::approve** enforces interactive user consent for CLI commands, while **HookPlan::approve_readonly** provides silent approval for streaming UI contexts.
- **ApprovedHookPlan** is an immutable structure defined in [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs) (lines 298–327) that freezes the hook list at the moment of approval.
- Execution handlers in [`src/commands/merge.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/merge.rs), [`src/commands/remove.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/remove.rs), and [`src/commands/worktree/switch.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/worktree/switch.rs) consume only **ApprovedHookPlan** references, ensuring strict adherence to the approved set.

## Frequently Asked Questions

### What prevents a project hook from executing without user approval in Worktrunk?

The type system and architectural design prevent unauthorized execution. Commands construct a mutable **HookPlan** but cannot execute hooks directly. They must call **HookPlan::approve** or **HookPlan::approve_readonly** to obtain an **ApprovedHookPlan**, which execution handlers in [`src/output/handlers.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/output/handlers.rs) require as proof of authorization. This immutable token pattern ensures that no hook runs without passing through the explicit approval gate.

### How does Worktrunk handle hooks in non-interactive UI components like the worktree picker?

For streaming or picker interfaces that cannot block for user input, Worktrunk uses **HookPlan::approve_readonly** as implemented in [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs). This method generates an **ApprovedHookPlan** without displaying a prompt, allowing the UI to remain responsive while still enforcing that only pre-discovered hooks execute. The implementation appears in [`src/commands/worktree/switch.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/worktree/switch.rs) (lines 1577–1615), where the picker UI schedules hooks using the approved plan without interactive blocking.

### Can hooks be added to the execution plan after user approval?

No. Once **HookPlan::approve** or **HookPlan::approve_readonly** returns an **ApprovedHookPlan**, the hook list is frozen. The execution phase accepts only immutable references to **ApprovedHookPlan**, as seen in [`src/commands/merge.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/merge.rs) and [`src/output/handlers.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/output/handlers.rs). This prevents any post-approval modification, ensuring that the exact hooks displayed during the approval prompt are the only hooks that execute.

### Where is the approval logic implemented in the Worktrunk codebase?

The core gating logic resides in [`src/commands/hook_plan.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/hook_plan.rs), specifically lines 298–327 where **HookPlan::approve** and **HookPlan::approve_readonly** convert mutable plans into immutable **ApprovedHookPlan** instances. The planning phase uses **HookPlanBuilder** (lines 90–110 in the same file), while enforcement occurs in command implementations like [`src/commands/merge.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/merge.rs) (lines 80–110), [`src/commands/remove.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/remove.rs) (lines 312–345), and the picker UI in [`src/commands/worktree/switch.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/worktree/switch.rs) (lines 1577–1615).