diff --git a/functions/_agents_repo_sync.fish b/functions/_agents_repo_sync.fish
index eb2772b..5832ef4 100644
--- a/functions/_agents_repo_sync.fish
+++ b/functions/_agents_repo_sync.fish
@@ -6,7 +6,9 @@
#
# DESCRIPTION
# Stages everything in
and commits it with . Shared by
-# agents-init and agents-vault.
+# agents-init, agents-vault and agents-cleanup. must be the root of
+# its own repository: a .git directory that is not a valid gitdir is
+# refused, because git would otherwise commit the enclosing repository.
#
# It never touches the network, and that is the point rather than an
# omission. Both callers run on every agent launch, synchronously, ahead
@@ -36,7 +38,8 @@
#
# EXIT STATUS
# 0 Committed, or nothing needed committing
-# 1 is not a git repository, arguments were missing, or the commit
+# 1 is not the root of its own git repository (a broken .git
+# directory inside another repository counts as not), arguments were missing, or the commit
# itself failed (e.g. a pre-commit/commit-msg hook rejected it)
# 2 A rebase is in progress; nothing committed, nothing touched
#
@@ -48,9 +51,12 @@
# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository"
function _agents_repo_sync --argument-names dir msg
test -n "$dir" -a -n "$msg"; or return 1
- test -d "$dir/.git"; or return 1
+ # Not `test -d "$dir/.git"`: a half-deleted .git directory passes that, and
+ # git then walks up, finds the enclosing repository and commits *that*.
+ # A valid repository rooted at reports its git dir as plain ".git".
+ test "$(git -C "$dir" rev-parse --git-dir 2>/dev/null)" = .git; or return 1
- # The guard above proved .git is a directory, so these are the same two
+ # The guard above proved has its own .git, so these are the same two
# paths `agents-vault --status` reports an unresolved rebase from.
if test -d "$dir/.git/rebase-merge"; or test -d "$dir/.git/rebase-apply"
echo "_agents_repo_sync: unresolved rebase in $dir; nothing committed" >&2
diff --git a/functions/agents-cleanup.fish b/functions/agents-cleanup.fish
index 6e4d905..c4feacc 100644
--- a/functions/agents-cleanup.fish
+++ b/functions/agents-cleanup.fish
@@ -136,7 +136,9 @@ function agents-cleanup --description 'undo agents-init: restore real files, arc
# Resolved, because link targets are compared after realpath, which
# also resolves any symlinked parent of $root.
set agents_dir (realpath -- "$agents_dir")
- test -d "$agents_dir/.git"; and set has_repo 1
+ # A .git that is not a valid gitdir (half-removed) is "not a repository";
+ # git would otherwise resolve to the enclosing project.
+ test "$(git -C "$agents_dir" rev-parse --git-dir 2>/dev/null)" = .git; and set has_repo 1
end
if test $has_repo -eq 1
diff --git a/tests/test-agents-cleanup.fish b/tests/test-agents-cleanup.fish
index a1d4606..7e72ab2 100644
--- a/tests/test-agents-cleanup.fish
+++ b/tests/test-agents-cleanup.fish
@@ -379,5 +379,19 @@ end
check "directive-only: deleted" false (test -e $v1/AGENTS.md -o -L $v1/AGENTS.md; and echo true; or echo false)
check "no directive: content identical" "$v2before" (string collect <$v2/AGENTS.md)
+section "agents-cleanup: invalid AGENTS/.git never commits the outer repo"
+fresh_state
+set -l i1 (scaffolded_repo)
+echo secret >$i1/untracked-secret.txt
+rm -rf $i1/AGENTS/.git/HEAD $i1/AGENTS/.git/objects
+set -l i1before (git -C $i1 rev-list --all | count)
+pushd $i1 >/dev/null
+set -l i1rc (agents-cleanup --silent 2>/dev/null; echo $status)
+popd >/dev/null
+check "invalid .git: exits 0" 0 "$i1rc"
+check "invalid .git: outer repo untouched" "$i1before" (git -C $i1 rev-list --all | count)
+check "invalid .git: no bundle" 0 (count $XDG_STATE_HOME/agents-cleanup/*.bundle 2>/dev/null)
+check "invalid .git: AGENTS/ removed" false (test -e $i1/AGENTS; and echo true; or echo false)
+
cleanup
report