1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
#!/usr/bin/env bash
# sweep-gitignore-tooling.sh — backfill the personal-tooling gitignore set
# across existing gitignore-mode AI projects.
#
# install-ai.sh sets up the ignore set at bootstrap, but a project bootstrapped
# before the set grew (or one that only ever ignored .ai/) is left exposed — an
# accidental `git add` or a /codify run can commit a personal CLAUDE.md, the
# private rule copies under .claude/, or an AGENTS.md. This sweep backfills the
# full set into every gitignore-mode project that's behind.
#
# For each AI project (a directory with .ai/protocols.org) under the search
# roots, if it's a git checkout in gitignore mode (.ai/ already appears in its
# .gitignore), ensure .ai/, .claude/, CLAUDE.md, and AGENTS.md are all ignored.
# Append only the missing lines, so a re-run is a no-op.
#
# Track-mode projects (.ai/ NOT in .gitignore) are skipped by design: they
# track their tooling on purpose — team repos sharing config with teammates who
# don't run rulesets, or private-remote personal repos where the history IS the
# project.
#
# A line added here only stops *future* commits. If a target path is already
# tracked, the ignore has no effect until it's untracked; the sweep warns so
# the path can be `git rm --cached`-ed by hand.
#
# Usage: sweep-gitignore-tooling.sh [--dry-run] [SEARCH_ROOT ...]
# --dry-run Report what would change without writing.
# SEARCH_ROOT ... Directories to scan (default: ~/code ~/projects ~/.emacs.d).
set -euo pipefail
IGNORE_SET=('.ai/' '.claude/' 'CLAUDE.md' 'AGENTS.md')
dry_run=0
roots=()
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=1 ;;
-h|--help)
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
-*)
echo "ERROR: unknown flag: $arg (use --help for usage)" >&2
exit 2
;;
*) roots+=("$arg") ;;
esac
done
if [ "${#roots[@]}" -eq 0 ]; then
roots=("$HOME/code" "$HOME/projects" "$HOME/.emacs.d")
fi
# Discover AI projects: a directory holding .ai/protocols.org.
mapfile -t projects < <(
for root in "${roots[@]}"; do
[ -d "$root" ] || continue
find "$root" -maxdepth 3 -type f -path '*/.ai/protocols.org' 2>/dev/null \
| sed 's|/\.ai/protocols\.org$||'
done | sort -u
)
swept=0 skipped=0 complete=0
for project in "${projects[@]}"; do
name="$(basename "$project")"
gi="$project/.gitignore"
if [ ! -d "$project/.git" ]; then
echo "skip $name — not a git checkout"
skipped=$((skipped + 1))
continue
fi
# Gitignore mode iff .ai/ is already ignored. Otherwise track-mode: leave it.
if [ ! -f "$gi" ] || ! grep -qFx '.ai/' "$gi"; then
echo "skip $name — track-mode (.ai/ not gitignored)"
skipped=$((skipped + 1))
continue
fi
needed=()
for pat in "${IGNORE_SET[@]}"; do
grep -qFx "$pat" "$gi" || needed+=("$pat")
done
if [ "${#needed[@]}" -eq 0 ]; then
echo "ok $name — already complete"
complete=$((complete + 1))
continue
fi
if [ "$dry_run" -eq 1 ]; then
echo "DRY $name — would add: ${needed[*]}"
else
{
echo ""
echo "# Claude Code per-project tooling (swept $(date +%Y-%m-%d))"
printf '%s\n' "${needed[@]}"
} >> "$gi"
echo "swept $name — added: ${needed[*]}"
fi
swept=$((swept + 1))
# Warn on any newly-ignored path that's already tracked — the ignore won't
# untrack it.
for pat in "${needed[@]}"; do
path="${pat%/}"
if git -C "$project" ls-files --error-unmatch "$path" >/dev/null 2>&1; then
echo " WARN $name: $path is currently tracked — 'git -C $project rm --cached -r $path' to untrack"
fi
done
done
echo
echo "Summary: $swept swept, $complete already complete, $skipped skipped (of ${#projects[@]} projects)."
[ "$dry_run" -eq 1 ] && echo "(dry-run — no files written)"
exit 0
|