How Project-Defined Hooks Are Gated and Approved in Worktrunk

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 (lines 90–110), the builder initializes with project and user configuration, then provides methods to add specific hook phases:

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 (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:

// 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:

// 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 (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 (lines 80–110), src/commands/remove.rs (lines 312–345), and 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. This type-level constraint prevents command implementations from adding unapproved hooks after the gate:

// 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 (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 (lines 298–327) that freezes the hook list at the moment of approval.
  • Execution handlers in src/commands/merge.rs, src/commands/remove.rs, and 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 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. 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 (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 and 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, 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 (lines 80–110), src/commands/remove.rs (lines 312–345), and the picker UI in src/commands/worktree/switch.rs (lines 1577–1615).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →