DEV Community

Cover image for Use Git Sparse-Checkout to Scope Agent Worktrees
Raylabs
Raylabs

Posted on Edited on Originally published at raylabs.app

Use Git Sparse-Checkout to Scope Agent Worktrees

An agent assigned to one package does not need every sibling package in its working directory. Use Git worktrees for separate branches and sparse-checkout for selected paths; treat both as workspace tools, not security controls.

Quick solution:

git worktree add --no-checkout -b agent/payments ../agent-payments main
cd ../agent-payments
git sparse-checkout set --cone packages/payments packages/shared
git checkout agent/payments
git sparse-checkout list
git status --short
Enter fullscreen mode Exit fullscreen mode

The first command creates a linked worktree without populating the full tree. sparse-checkout set records the paths and git checkout fills the worktree with the selected files. Cone mode keeps top-level files and the files needed in leading directories, while including the chosen directories recursively. Git documents these cone rules.

Why combine worktrees and sparse-checkout?

A worktree gives each task its own directory, branch, index, and HEAD, while linked worktrees share the repository's Git object database. Git's worktree documentation describes these linked working directories. An agent can edit a feature branch without switching the main working copy away from another task. A Multi Agent Review Pipeline can structure review and handoffs across those branches, while sparse-checkout still determines which tracked paths are populated. Sparse-checkout then limits which tracked paths Git populates in that worktree.

This can make the files visible to a tool easier to inspect and can reduce working-tree disk use. It does not automatically reduce model tokens or API cost. That depends on how an editor or agent gathers context. Sparse-checkout also does not reduce commit history; it changes the populated paths. A shallow clone addresses history depth, which is a separate choice. For broader repository-scale context, GitHub has a sparse-checkout overview.

Use a sparse worktree when a task has a clear package boundary and its direct dependencies are known. Use a full checkout when a task regularly crosses package boundaries, runs repository-wide generators, or needs broad search. Keep a separate integration check against the complete repository when package-local tests cannot represent the whole workspace.

Create two linked worktrees with different scopes

Start from the main repository root. Use distinct branch and directory names for the worktrees. The --no-checkout option avoids first populating every tracked file; configure the sparse set before checking out the branch:

git worktree add --no-checkout -b agent/auth ../agent-auth main
git -C ../agent-auth sparse-checkout set --cone packages/auth packages/shared
git -C ../agent-auth checkout agent/auth

git worktree add --no-checkout -b agent/payments ../agent-payments main
git -C ../agent-payments sparse-checkout set --cone packages/payments packages/shared
git -C ../agent-payments checkout agent/payments
Enter fullscreen mode Exit fullscreen mode

Now inspect each worktree from the repository root:

git -C ../agent-auth worktree list
git -C ../agent-auth sparse-checkout list
git -C ../agent-payments sparse-checkout list
Enter fullscreen mode Exit fullscreen mode

The first list confirms that both paths are attached to the same repository. The sparse lists should show packages/auth and packages/shared for the authentication task, and packages/payments and packages/shared for the payments task. The shared package is deliberately present in both because both tasks depend on it. Each worktree can be changed independently by running git sparse-checkout set from that worktree or with git -C.

A linked worktree cannot check out a branch that is already checked out elsewhere. Give each task a distinct branch, or use a detached worktree when a branch is not needed. git worktree list is the quickest way to find which worktree owns a branch before trying to add it again.

Per-worktree configuration and root-file visibility

The sparse set belongs to the worktree. The git sparse-checkout set command enables Git's worktree-specific configuration support when needed and stores a sparse pattern file for that worktree. Do not guess where those files live: Git resolves paths according to the linked worktree's git directory. Run these commands from the repository root:

git -C ../agent-auth rev-parse --git-path config.worktree
git -C ../agent-auth rev-parse --git-path info/sparse-checkout
git -C ../agent-payments rev-parse --git-path config.worktree
git -C ../agent-payments rev-parse --git-path info/sparse-checkout
Enter fullscreen mode Exit fullscreen mode

For linked worktrees the resolved paths are under separate entries in the common repository's worktrees directory. The main worktree and linked worktrees do not all use a literal .git/config.worktree path. Git's worktree configuration documentation describes this distinction. Checking the resolved paths helps diagnose a setup where changing one worktree unexpectedly changes another.

Cone mode always includes files at the repository top level. When a selected directory is nested, Git also includes files immediately inside its leading directories so the selected path can be reached. It does not recursively include every sibling package. For example, selecting packages/auth makes the target tree available while leaving the deep contents of packages/payments absent; files immediately in a leading directory may still be visible. Do not describe the result as a list of only the exact files under the selected package.

This root visibility often helps package tools read files such as package.json, lockfiles, and workspace manifests. It does not guarantee a monorepo build will work: a root script might discover packages by scanning all package directories, and a workspace manager may expect manifests for packages that are outside the sparse set. Test the actual command the agent must run rather than assuming that visible root configuration implies every workspace dependency is present.

Hydrate required dependencies deliberately

Start with the target package and known shared dependencies. If a build identifies another required directory, add it to the existing set rather than replacing the set:

git -C ../agent-auth sparse-checkout add packages/internal-tools
git -C ../agent-auth sparse-checkout list
git -C ../agent-auth status --short
Enter fullscreen mode Exit fullscreen mode

set replaces the selected directories; add extends them. This distinction matters during repair. Re-running set packages/auth would drop packages/shared, while add packages/internal-tools keeps both existing paths. The add command requires sparse-checkout to be active already.

A dependency can mean more than a runtime library. Build plugins, generated schemas, code-generation templates, package manifests, test fixtures, and repository scripts can live in separate directories. Trace the command's actual inputs and add only the paths it needs. Then rerun the same test or build. If it passes locally but fails in full CI, inspect the complete CI job for an implicit dependency on an unpopulated sibling or a root script that assumes all packages exist.

If the task grows beyond its original scope, expand the set deliberately or move the work to a full checkout. Do not keep adding broad parent directories just to silence unexplained errors. Record which added path fixed the command so the next task can start with a better-scoped set.

Choose the right checkout pattern

Option When to use it Trade-off or failure mode Recommendation
Full worktree Work spans many packages or uses repository-wide tools More tracked paths are populated in the working directory Use when the task boundary is uncertain
Cone-mode sparse worktree An agent owns a known package and a small dependency set Root and leading-directory files remain visible; missing siblings can break workspace-wide commands Use for bounded package work, then run integration checks in CI
Shallow clone (--depth 1) A job needs a checkout but not older commit history History-dependent commands such as some rebases or release checks can fail Use only when reduced history is acceptable; it does not select package paths

Sparse-checkout, worktrees, and shallow history solve different problems. A worktree separates branch state. Sparse-checkout selects populated paths. A shallow clone limits history. Combine them only when each boundary is understood and the build still has the files and history it needs.

Validate the diff separately

Sparse-checkout is not access control. A process with shell access can change the sparse set, disable sparse-checkout, or ask Git to read objects outside the populated paths. Use operating-system permissions, a container or sandbox, and credential boundaries to limit access. Separately, enforce a path allowlist on the proposed diff so an unrelated edit cannot enter the pull request unnoticed.

The Bash check below reads filenames as NUL-delimited records. That keeps spaces, tabs, and newlines in filenames from being treated as separators. It writes git diff output to a temporary file so a failed diff exits before the allowlist check can report success. In CI, set BASE_REF to the trusted target-branch commit or ref that the job fetched.

#!/usr/bin/env bash
set -euo pipefail

approved_prefix='packages/auth/'
base_ref="${BASE_REF:-main}"
changed_paths="$(mktemp)"
trap 'rm -f "$changed_paths"' EXIT

git rev-parse --verify "${base_ref}^{commit}" >/dev/null
git diff --name-only -z "${base_ref}...HEAD" > "$changed_paths"

path_is_allowed() {
  [[ "$1" == "$approved_prefix"* ]]
}

path_is_allowed 'packages/auth/src/example.ts' || {
  printf 'Allowed path was rejected\n' >&2
  exit 1
}
if path_is_allowed 'packages/auth-admin/config.yml'; then
  printf 'Prefix collision was allowed\n' >&2
  exit 1
}
if path_is_allowed 'packages/payments/config.yml'; then
  printf 'Unrelated path was allowed\n' >&2
  exit 1
}

unauthorized=()
while IFS= read -r -d '' file; do
  if ! path_is_allowed "$file"; then
    unauthorized+=("$file")
  fi
done < "$changed_paths"

if ((${#unauthorized[@]})); then
  printf 'Out-of-scope path: %q\n' "${unauthorized[@]}" >&2
  exit 1
fi
printf 'All changed files are within %s\n' "$approved_prefix"
Enter fullscreen mode Exit fullscreen mode

The trailing slash in packages/auth/ is the directory boundary: packages/auth/src/example.ts passes, but packages/auth-admin/config.yml does not. The test assertions exercise that boundary before checking the actual diff. git diff here compares committed changes from the selected base to HEAD; it does not include uncommitted or staged-only changes. If your policy must inspect those too, include the index and working tree in a separate check.

A successful script means the diff passed this path rule. It does not prove the code is safe, the test suite passed, or the agent could not read a secret. Keep those checks separate. If a merge, rebase, or conflict resolution temporarily brings excluded files into the working tree, review the changed paths and run git sparse-checkout reapply when appropriate after resolving the conflict. Git's sparse-checkout reference describes these operations and their limits.

Verification checklist

  • Confirm git worktree list shows the task directory and its intended branch.
  • Run git sparse-checkout list inside each worktree and confirm the selected paths differ as expected.
  • Check that required root files, leading-directory files, and shared dependencies are present.
  • Run the package's tests and build, then run the integration job that scans the full repository.
  • Add one allowed path and one unrelated path in a disposable branch; confirm the allowlist accepts the first and rejects the second.
  • Verify that the policy check fails if the base ref is missing or git diff cannot produce the changed-path file.

Use the smallest set that supports the task, then widen it when the build evidence shows a missing dependency. That keeps the workspace focused without confusing convenience with enforcement.

Top comments (2)

Collapse
 
vishisht_choudhary_a12d08 profile image
Vishisht Choudhary •

Watch out: the agent greps a sparse tree, finds 2 callers, and confidently breaks the other 40. git grep <pattern> HEAD saves lives.

Collapse
 
raylabs profile image
Raylabs •

Haha spot on, the classic "confidently wrong" refactor trap!

Using git grep <pattern> HEAD to scan the whole tree without blowing up the sparse checkout is a lifesaver. This exact blindspot is also why we run an independent read-only reviewer agent to audit blast radius before anything merges: raylabs.app/articles/multi-
agent-review-pipeline/

Thanks for dropping this tip, @vishisht_choudhary_a12d08 !