diff --git a/completions/auto-pull.fish b/completions/auto-pull.fish
new file mode 100644
index 0000000..67803f7
--- /dev/null
+++ b/completions/auto-pull.fish
@@ -0,0 +1,33 @@
+# Copyright (C) 2026 Rootiest
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# Completions for the `auto-pull` registry command.
+
+function __auto_pull_registered
+ set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list"
+ test -r "$list"; or return
+ for l in (command cat "$list" 2>/dev/null)
+ test -n "$l"; or continue
+ printf '%s\t%s\n' (path basename "$l") "$l"
+ end
+end
+
+set -l subcmds list add remove status
+
+# Subcommands (only as the first argument).
+complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \
+ -a list -d 'Show registered repos'
+complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \
+ -a add -d "Register a repo (default: current)"
+complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \
+ -a remove -d 'Unregister a repo'
+complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \
+ -a status -d 'Show enabled state and registry path'
+complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \
+ -s h -l help -d 'Show help'
+
+# `add` takes a directory path.
+complete -c auto-pull -n "__fish_seen_subcommand_from add" -a '(__fish_complete_directories)'
+
+# `remove` completes registered repo basenames.
+complete -c auto-pull -f -n "__fish_seen_subcommand_from remove" -a '(__auto_pull_registered)'
diff --git a/conf.d/auto-pull.fish b/conf.d/auto-pull.fish
new file mode 100644
index 0000000..4096ed2
--- /dev/null
+++ b/conf.d/auto-pull.fish
@@ -0,0 +1,62 @@
+# Copyright (C) 2026 Rootiest
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# ╭──────────────────────────────────────────────────────────╮
+# │ Auto-Pull (C2 — Autoexec) │
+# ╰──────────────────────────────────────────────────────────╯
+#
+# Background, fast-forward-only `git pull` for opted-in repositories. Fires
+# when the working directory enters the fish-config repo (always covered as a
+# baseline) or any repository registered via the `auto-pull` command. The
+# actual sync is delegated to `_auto_pull_sync`, which only ever fast-forwards
+# a clean repo — see that function for the full safety contract.
+#
+# Manage the registry with: auto-pull add / remove / list / status
+
+# C2 guard: when auto-execution is disabled, do not register the handler.
+__fish_config_op_enabled __fish_config_op_autoexec; or exit
+
+# SYNOPSIS
+# __auto_pull_on_pwd (event handler, --on-variable PWD)
+#
+# DESCRIPTION
+# On every directory change, checks whether the new $PWD is inside a
+# registered repository (or the fish-config baseline) and, if so, kicks a
+# background fast-forward. A throttle global ensures it fires once per
+# repository entry rather than on every sub-directory change.
+function __auto_pull_on_pwd --on-variable PWD
+ set -q __fish_config_dir; or set -g __fish_config_dir $XDG_CONFIG_HOME/fish
+
+ set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list"
+
+ # Candidate roots: the fish-config repo plus the user's registry.
+ set -l roots $__fish_config_dir
+ test -r "$list"; and set -a roots (command cat "$list" 2>/dev/null)
+
+ # Find the registered root containing $PWD (exact match or a sub-directory).
+ set -l hit
+ for r in $roots
+ test -n "$r"; or continue
+ if test "$PWD" = "$r"; or string match -q -- "$r/*" "$PWD"
+ set hit "$r"
+ break
+ end
+ end
+
+ # Outside every registered repo: clear the throttle so re-entry re-syncs.
+ if test -z "$hit"
+ set -e __auto_pull_last
+ return
+ end
+
+ # Already synced for this repo entry — skip until we leave and return.
+ test "$hit" = "$__auto_pull_last"; and return
+ set -g __auto_pull_last "$hit"
+
+ # Background fast-forward. A --no-config child sources just the worker so
+ # it stays fast and free of shell side-effects; real `git`, no shadows.
+ set -l worker "$__fish_config_dir/functions/_auto_pull_sync.fish"
+ test -r "$worker"; or return
+ fish --no-config -c 'source $argv[1]; _auto_pull_sync $argv[2]' -- "$worker" "$hit" &
+ disown 2>/dev/null
+end
diff --git a/functions/_auto_pull_sync.fish b/functions/_auto_pull_sync.fish
new file mode 100644
index 0000000..d03be5e
--- /dev/null
+++ b/functions/_auto_pull_sync.fish
@@ -0,0 +1,47 @@
+# Copyright (C) 2026 Rootiest
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# SYNOPSIS
+# _auto_pull_sync
+#
+# DESCRIPTION
+# Best-effort, side-effect-safe fast-forward of a single git repository.
+# Performs a fetch and a fast-forward-only merge of the current branch's
+# upstream, but only when it is completely safe to do so: the work tree
+# must be clean, the branch must have an upstream, and the merge must be a
+# true fast-forward. Any failed precondition makes the function a no-op.
+# It never rebases, never creates a merge commit, and never overwrites
+# committed or uncommitted work — worst case it does nothing.
+#
+# Intended to be invoked in the background by the auto-pull PWD handler
+# (conf.d/auto-pull.fish), but safe to call directly.
+#
+# ARGUMENTS
+# dir Absolute path to the git repository to fast-forward
+#
+# RETURNS
+# 0 Fast-forward applied (or already up to date)
+# 1 A precondition failed; nothing was changed
+#
+# EXAMPLE
+# _auto_pull_sync ~/.config/fish
+function _auto_pull_sync --description 'Fast-forward a repo when clean and a ff is possible'
+ set -l d $argv[1]
+ test -n "$d"; or return 1
+
+ # Must be a git work tree.
+ command git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1; or return 1
+
+ # Work tree must be clean (no staged or unstaged changes).
+ command git -C "$d" diff --quiet 2>/dev/null
+ and command git -C "$d" diff --cached --quiet 2>/dev/null
+ or return 1
+
+ # Current branch must have an upstream to fast-forward against.
+ command git -C "$d" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
+ or return 1
+
+ # Fetch, then fast-forward only. --ff-only aborts (no-op) on any divergence.
+ command git -C "$d" fetch -q 2>/dev/null; or return 1
+ command git -C "$d" merge -q --ff-only '@{u}' >/dev/null 2>&1
+end
diff --git a/functions/auto-pull.fish b/functions/auto-pull.fish
new file mode 100644
index 0000000..0378503
--- /dev/null
+++ b/functions/auto-pull.fish
@@ -0,0 +1,150 @@
+# Copyright (C) 2026 Rootiest
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# SYNOPSIS
+# auto-pull [list]
+# auto-pull add [PATH]
+# auto-pull remove
+# auto-pull status
+#
+# DESCRIPTION
+# Manages the auto-pull registry: the list of repositories that are
+# background fast-forwarded when you enter them (see conf.d/auto-pull.fish
+# and _auto_pull_sync). The fish-config repo is always covered as a baseline
+# and does not need to be added. The registry is a plain text file, one
+# absolute git-toplevel path per line, stored machine-locally in
+# ~/.config/.user-dots/fish/auto-pull.list (never committed to the config).
+#
+# Registry management works regardless of the C2 auto-execution guard; only
+# the background sync itself is gated by __fish_config_op_autoexec.
+#
+# ARGUMENTS
+# list Show registered repos (default when no subcommand given)
+# add [PATH] Register PATH's git root; defaults to the current repo
+# remove Unregister by basename or exact path
+# status Show whether auto-pull is enabled and the registry path
+# -h, --help Show this help message
+#
+# RETURNS
+# 0 Subcommand succeeded
+# 1 Bad usage, target is not a git repo, or target not registered
+#
+# EXAMPLE
+# cd ~/src/qmk_firmware; and auto-pull add
+# auto-pull add ~/work/api
+# auto-pull list
+# auto-pull remove qmk_firmware
+function auto-pull --description 'Manage the auto-pull repository registry'
+ set -l c_head (set_color --bold cyan)
+ set -l c_cmd (set_color --bold white)
+ set -l c_flag (set_color yellow)
+ set -l c_ok (set_color green)
+ set -l c_warn (set_color yellow)
+ set -l c_err (set_color red)
+ set -l c_dim (set_color brblack)
+ set -l c_reset (set_color normal)
+
+ set -q __fish_config_dir; or set -l __fish_config_dir $XDG_CONFIG_HOME/fish
+ set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list"
+
+ set -l cmd $argv[1]
+ set -e argv[1]
+
+ if test "$cmd" = -h; or test "$cmd" = --help; or contains -- -h $argv; or contains -- --help $argv
+ echo "$c_head""Usage:$c_reset $c_cmd""auto-pull$c_reset $c_flag""[list | add | remove | status]$c_reset $c_dim""[PATH|NAME]$c_reset"
+ echo
+ echo " Manage the registry of repos that are background fast-forwarded on entry."
+ echo
+ echo "$c_head""Subcommands:$c_reset"
+ echo " $c_flag""list$c_reset Show registered repos (default)"
+ echo " $c_flag""add$c_reset $c_dim""[PATH]$c_reset Register PATH's git root (default: current repo)"
+ echo " $c_flag""remove$c_reset $c_dim""$c_reset Unregister by basename or exact path"
+ echo " $c_flag""status$c_reset Show enabled/disabled state and registry path"
+ echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
+ return 0
+ end
+
+ switch "$cmd"
+ case '' list
+ echo "$c_head""Auto-pull registry:$c_reset"
+ echo " $c_dim$__fish_config_dir$c_reset $c_dim(fish-config, baseline)$c_reset"
+ if test -r "$list"
+ for l in (command cat "$list" 2>/dev/null)
+ test -n "$l"; or continue
+ echo " $l $c_dim("(path basename "$l")")$c_reset"
+ end
+ end
+ return 0
+
+ case add
+ set -l target $argv[1]
+ test -n "$target"; or set target $PWD
+ set -l root (command git -C "$target" rev-parse --show-toplevel 2>/dev/null)
+ if test -z "$root"
+ echo "$c_err""auto-pull: not a git repository: $target$c_reset" >&2
+ return 1
+ end
+ if test "$root" = "$__fish_config_dir"
+ echo "$c_warn""auto-pull: fish-config is always covered (baseline); nothing to add$c_reset"
+ return 0
+ end
+ command mkdir -p (path dirname "$list")
+ test -f "$list"; or command touch "$list"
+ if contains -- "$root" (command cat "$list" 2>/dev/null)
+ echo "$c_dim""auto-pull: already registered: $root$c_reset"
+ return 0
+ end
+ echo "$root" >>"$list"
+ echo "$c_ok""→ auto-pull added: $root$c_reset"
+ return 0
+
+ case remove rm
+ set -l target $argv[1]
+ if test -z "$target"
+ echo "$c_err""auto-pull: usage: auto-pull remove $c_reset" >&2
+ return 1
+ end
+ if not test -r "$list"
+ echo "$c_dim""auto-pull: registry is empty$c_reset"
+ return 1
+ end
+ set -l kept
+ set -l removed
+ for l in (command cat "$list" 2>/dev/null)
+ test -n "$l"; or continue
+ if test "$l" = "$target"; or test (path basename "$l") = "$target"
+ set -a removed "$l"
+ else
+ set -a kept "$l"
+ end
+ end
+ if test -z "$removed"
+ echo "$c_err""auto-pull: not registered: $target$c_reset" >&2
+ return 1
+ end
+ if test -n "$kept"
+ printf '%s\n' $kept >"$list"
+ else
+ printf '' >"$list"
+ end
+ echo "$c_warn""→ auto-pull removed: $removed$c_reset"
+ return 0
+
+ case status
+ if __fish_config_op_enabled __fish_config_op_autoexec
+ echo "$c_ok""auto-pull: ENABLED$c_reset $c_dim(C2 auto-execution on)$c_reset"
+ else
+ echo "$c_warn""auto-pull: DISABLED$c_reset $c_dim(via __fish_config_op_autoexec)$c_reset"
+ end
+ set -l repos
+ test -r "$list"; and set repos (command cat "$list" 2>/dev/null | string match -rv '^\s*$')
+ echo " registered repos: "(count $repos)" $c_dim(+ fish-config baseline)$c_reset"
+ echo " list file: $c_dim$list$c_reset"
+ return 0
+
+ case '*'
+ echo "$c_err""auto-pull: unknown subcommand: $cmd$c_reset" >&2
+ echo "$c_dim""try: auto-pull --help$c_reset" >&2
+ return 1
+ end
+end