Bỏ qua tới nội dung
Chinh Hoang
Quay lại

How a dotfiles auto-commit deleted three of my coding agents' rules, and the guard that blocks it now

Markdown
Mở trong ChatGPTMở trong Claude

Bài này hiện chỉ có bản tiếng Anh.

Mục lục
  1. Does this guide apply to you?
  2. The setup
  3. Timeline (+07, from git log and the incident note of that night)
  4. Root cause
  5. The guardrail
  6. How to check it blocks
  7. What it costs

On 23 September 2026 at 01:20, a 15-minute auto-commit job pushed a stale copy of my coding agents’ rule folder: 3 of 28 rules deleted, 12 rolled back to older text. A guard now runs before every auto-commit and refuses that shape; a replay of the failure prints BLOCKED and exits 1.

Does this guide apply to you?

The setup

My agents read their global rules from ~/.agents/rules. ~/.agents is a symlink into my chezmoi source repo. A timer ran every 15 minutes: chezmoi re-add, commit, push. Every machine pulls that repo and regenerates each agent’s rule files from it.

Timeline (+07, from git log and the incident note of that night)

Root cause

Two things lined up. rsync --delete wrote a stale tree through a symlink into a git working tree. And chezmoi’s git.autoCommit = true with autoPush = true made chezmoi re-add commit and push the whole source before my script ran anything.

My first guard sat after re-add, so it never saw the change. Testing it at 01:50, chezmoi committed and pushed a deleted rule by itself: Remove agents/rules/anti-overengineering.md.

The guardrail

  1. autoCommit = false and autoPush = false, committed at 01:52. The sync script is now the only automatic commit path.
  2. The guard runs after git add -A and before the commit. It blocks a deleted or renamed file under protected paths, more than 15 changed files, a modified file whose mtime is older than its last commit, a file that shrank below half, secret-looking names and backup files.

The mtime check catches copies. rsync -a, rclone or a restore keeps the old mtime; a real edit has a new one.

while IFS=$'\t' read -r st path newpath; do
  [ -n "$st" ] || continue
  count=$((count+1))
  case "$st" in
    D*) protected "$path" && reasons+=("deletes protected file: $path") ;;
    R*) protected "$path" && reasons+=("renames protected file: $path -> $newpath") ;;
    M)
      # A modified file whose mtime predates the commit that last touched it was
      # copied in from an older snapshot (rsync -a, rclone, restore), not edited.
      if [ -f "$path" ]; then
        last="$(git log -1 --format=%ct -- "$path" 2>/dev/null || echo 0)"
        [ "$(mtime_of "$path")" -lt "$last" ] && reasons+=("$path: mtime older than its last commit (stale copy?)")
        old_lines="$(git show "HEAD:$path" 2>/dev/null | wc -l)"
        new_lines="$(wc -l <"$path")"
        if [ "$old_lines" -ge "$SHRINK_MIN_LINES" ] && [ $((new_lines * 2)) -lt "$old_lines" ]; then
          reasons+=("$path: shrank from $old_lines to $new_lines lines (truncated copy?)")
        fi
      fi ;;
  esac
  # … secret-name and backup-file checks, then the file-count check

How to check it blocks

Replay the failure in a throwaway repo: commit three rules, then rsync -a --delete an older copy over them with an August mtime and one rule missing. Save the full guard (67 lines) as guard.sh next to this script.

#!/usr/bin/env bash
# Reproduce the 23 Sep 2026 failure in a throwaway repo and show the guard refusing it.
# Usage: bash demo-stale-copy.sh   (needs git, rsync; guard.sh = copy of the real guard)
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
t=$(mktemp -d); trap 'rm -rf "$t"' EXIT
git init -q "$t/repo"; cd "$t/repo"
git config user.email demo@example.com; git config user.name demo
mkdir -p agents/rules
for r in tdd review banned-typefaces; do printf '# %s\nline 2 of the current rule\n' "$r" > agents/rules/$r.md; done
git add -A; git commit -qm "current rules"
# A stale copy from another machine: older text, older mtime, one rule missing.
mkdir -p "$t/stale/rules"
for r in tdd review; do printf '# %s\nold line 2\n' "$r" > "$t/stale/rules/$r.md"; done
touch -d '2026-08-26 10:00' "$t"/stale/rules/*.md
rsync -a --delete "$t/stale/rules/" agents/rules/
git add -A
echo "\$ git diff --cached --name-status"; git diff --cached --name-status
echo "\$ bash guard.sh; echo exit=\$?"
set +e; bash "$here/guard.sh" .; echo "exit=$?"
$ git diff --cached --name-status
D	agents/rules/banned-typefaces.md
M	agents/rules/review.md
M	agents/rules/tdd.md
$ bash guard.sh; echo exit=$?
chezmoi-sync-guard: BLOCKED — working tree looks wrong, nothing committed or pushed
 - deletes protected file: agents/rules/banned-typefaces.md
 - agents/rules/review.md: mtime older than its last commit (stale copy?)
 - agents/rules/tdd.md: mtime older than its last commit (stale copy?)
exit=1

Two lines with local paths, before exit=1, were cut. The guard’s own test script covers 11 cases:

$ bash bin/test-chezmoi-sync-guard.sh | tail -1
passed=11 failed=0

What it costs


Chia sẻ bài viết:

Bài trước
Does a Simplified Technical English prompt make LLM output shorter? Five runs each