Skip to content

The three Git roots

 2 min A linked worktree has shared Git state, per-worktree Git state, and a visible worktree root. Hook installation crosses all three.
Published

Jul 21, 2026

In a linked worktree, .git is a file containing a gitdir: pointer, usually into the main repository’s .git/worktrees/<name>/. Following that pointer does not reveal one canonical place for repository state. It reveals three places with different owners:

Without a configured total-state backend, the current Python implementation of Spice uses all three. Its path resolver puts shared managed state under <git-common-dir>/.spice, durable worktree-local state under <git-dir>/.spice, and mutable visible state under <worktree>/.spice.

Hook installation belongs to the third root. The initializer plans this hook-selection spine in order:

  1. enable extensions.worktreeConfig in the common config;
  2. record core.bare=false in this worktree’s config;
  3. write executable shims under <worktree>/.spice/hooks/;
  4. set this worktree’s core.hooksPath to .spice/hooks.

The hook files exist before Git is pointed at them, and one worktree can be initialized or reversed without silently changing which hooks another worktree runs. The one shared mutation, enabling worktree config, is tracked separately from the worktree-local selection that depends on it.

This corrected an earlier design of mine that put the hook directory under the common Git dir and selected it through shared local config. That design handled the .git indirection but assigned a worktree choice to repository-wide state. It solved the path problem and missed the ownership problem.

The portable rule is not “always use the common dir.” Ask which scope owns the fact, then resolve that scope through Git. Objects and refs are common. HEAD, the index, worktree config, and installation receipts are not. Files an operator should see in a checkout may belong in the worktree itself. .git is not one directory, and “the real Git directory” is usually the wrong question.