From 49e5e7b204184c0fdf3095c85259cc4600ed8cc2 Mon Sep 17 00:00:00 2001 From: Meni Livne Date: Mon, 19 Oct 2026 08:12:31 -0700 Subject: [PATCH] refactor(template): isolate sync-agent-ignore to template repository Move sync-agent-ignore.sh out of template/scripts so created repositories do not inherit the sync script or its Justfile recipes. The template repository retains scripts/sync-agent-ignore.sh and its local Justfile recipes to keep .claude/settings.json in sync with .agentignore. AI-Reasoning: https://github.com/menil/project-template/blob/ai-reasoning/refactor-isolate-sync-agent-ignore.md --- Justfile | 11 +++- scripts/sync-agent-ignore.sh | 72 ++++++++++++++++++++++++++- template/Justfile | 12 +---- template/README.md.jinja | 8 ++- template/scripts/sync-agent-ignore.sh | 43 ---------------- 5 files changed, 86 insertions(+), 60 deletions(-) mode change 120000 => 100755 scripts/sync-agent-ignore.sh delete mode 100755 template/scripts/sync-agent-ignore.sh diff --git a/Justfile b/Justfile index d4aab2a..a568027 100644 --- a/Justfile +++ b/Justfile @@ -30,7 +30,7 @@ test-template: # 1. Verify root developer tooling files are symlinks to template/ failed=0 - for symlink in .agentignore .envrc .githooks/commit-msg .githooks/pre-commit scripts/sync-agent-ignore.sh .claude/settings.json; do + for symlink in .agentignore .envrc .githooks/commit-msg .githooks/pre-commit .claude/settings.json; do if [[ ! -L "$symlink" ]]; then echo "error: root file '$symlink' is not a symlink to template/" >&2 failed=1 @@ -41,13 +41,21 @@ test-template: fi diff -u .gitignore template/.gitignore + # Verify sync-agent-ignore.sh is tracked as an executable regular file (not a symlink) + test -f scripts/sync-agent-ignore.sh + test ! -L scripts/sync-agent-ignore.sh + test -x scripts/sync-agent-ignore.sh + # 2. Test default template rendering (enable_pr_reviews=true) copier copy --defaults --trust . "$TMPDIR/rendered-default" test -f "$TMPDIR/rendered-default/README.md" test -f "$TMPDIR/rendered-default/shell.nix" + test -f "$TMPDIR/rendered-default/.claude/settings.json" test -f "$TMPDIR/rendered-default/.github/workflows/pr-review.yml" test ! -f "$TMPDIR/rendered-default/copier.yml" test ! -f "$TMPDIR/rendered-default/scripts/create-project.sh" + test ! -f "$TMPDIR/rendered-default/scripts/sync-agent-ignore.sh" + test ! -d "$TMPDIR/rendered-default/scripts" test ! -d "$TMPDIR/rendered-default/.beads" # Verify rendered project passes its own validation suite @@ -61,6 +69,7 @@ test-template: # 4. Test scaffolding script dry run ./scripts/create-project.sh --dry-run --defaults --template . "$TMPDIR/dry-run-app" test -f "$TMPDIR/dry-run-app/README.md" + test ! -d "$TMPDIR/dry-run-app/scripts" # 5. Verify scaffolding script rejects target paths inside template directory (both shallow and multi-level) err_output=$(./scripts/create-project.sh --dry-run --defaults --template . "./internal-target-error" 2>&1 || true) diff --git a/scripts/sync-agent-ignore.sh b/scripts/sync-agent-ignore.sh deleted file mode 120000 index 8f39eff..0000000 --- a/scripts/sync-agent-ignore.sh +++ /dev/null @@ -1 +0,0 @@ -../template/scripts/sync-agent-ignore.sh \ No newline at end of file diff --git a/scripts/sync-agent-ignore.sh b/scripts/sync-agent-ignore.sh new file mode 100755 index 0000000..5bf585a --- /dev/null +++ b/scripts/sync-agent-ignore.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Template-maintenance script: regenerates .claude/settings.json's Read-deny +# rules from .agentignore so Claude Code enforces the same patterns as the +# gitignore-syntax .agentignore without hand-duplicating them. +# Writes through root .claude/settings.json symlink to update template/.claude/settings.json. +# +# Usage: +# scripts/sync-agent-ignore.sh # regenerate .claude/settings.json +# scripts/sync-agent-ignore.sh --check # verify it's already in sync (no write) +set -euo pipefail + +CDPATH='' cd -- "$(dirname "${BASH_SOURCE[0]}")/.." + +if ! command -v jq >/dev/null 2>&1; then + echo "error: required command 'jq' is not installed or not in PATH" >&2 + echo "tip: run inside 'nix-shell' or install jq via your system package manager" >&2 + exit 1 +fi + +if [[ ! -f .agentignore ]]; then + echo "error: .agentignore not found in $(pwd)" >&2 + exit 1 +fi + +deny=() +while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + # Skip empty lines, lines with only whitespace, and comments (including indented) + [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue + # Strip leading and trailing whitespace + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + if [[ "$line" == */ ]]; then + deny+=("Read(./**/${line%/}/**)") + else + deny+=("Read(./**/${line})") + fi +done < .agentignore + +# Handle empty pattern list defensively to generate valid JSON ({deny: []}) +# without triggering Bash 3.2 unbound variable errors under set -u +if [[ ${#deny[@]} -eq 0 ]]; then + generated='{"permissions":{"deny":[]}}' +else + generated="$(printf '%s\n' "${deny[@]}" | jq -R . | jq -s '{permissions: {deny: .}}')" +fi + +case "${1:-}" in + "") + mkdir -p .claude + printf '%s\n' "$generated" > .claude/settings.json + echo "Regenerated .claude/settings.json from .agentignore" + ;; + --check) + current="$(tr -d '\r' < .claude/settings.json 2>/dev/null || true)" + if [[ "$current" != "$generated" ]]; then + echo "error: .claude/settings.json is stale — run 'just sync-agent-ignore' to regenerate" >&2 + exit 1 + fi + echo ".claude/settings.json is in sync with .agentignore" + ;; + -h|--help) + echo "Usage: $(basename "${BASH_SOURCE[0]}") [--check]" + exit 0 + ;; + *) + echo "error: unrecognized argument '$1'" >&2 + echo "Usage: $(basename "${BASH_SOURCE[0]}") [--check]" >&2 + exit 1 + ;; +esac diff --git a/template/Justfile b/template/Justfile index 4500d38..a12830d 100644 --- a/template/Justfile +++ b/template/Justfile @@ -5,20 +5,12 @@ default: @just --list # Format code and configuration files -format: sync-agent-ignore +format: @echo "No formatter configured yet. Customize this recipe in the Justfile!" # Run code and markdown linting checks lint: @echo "No linter configured yet. Customize this recipe in the Justfile!" -# Regenerate .claude/settings.json's Read-deny rules from .agentignore -sync-agent-ignore: - @scripts/sync-agent-ignore.sh - -# Check that .claude/settings.json is in sync with .agentignore -check-agent-ignore-sync: - @scripts/sync-agent-ignore.sh --check - # Run all local checks (tests, format checks, lints) -validate: check-agent-ignore-sync lint +validate: lint diff --git a/template/README.md.jinja b/template/README.md.jinja index 8edd3eb..0445e15 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -41,10 +41,8 @@ nix-shell ### Task Runner (`Justfile`) The following tasks are available via `just`: - `just`: List all available tasks. -- `just format`: Format code and configuration files (also regenerates `.claude/settings.json` from `.agentignore`). +- `just format`: Format code and configuration files. - `just lint`: Run code and markdown linters. -- `just sync-agent-ignore`: Regenerate `.claude/settings.json`'s `Read` deny rules from `.agentignore`. -- `just check-agent-ignore-sync`: Verify `.claude/settings.json` is in sync with `.agentignore` (no write). - `just validate`: Execute all formatting, linting, and verification checks. ### Git Hook Checks @@ -62,9 +60,9 @@ These hooks only run locally and can be skipped (`git commit --no-verify`) or si - **Google Antigravity**: `.antigravityignore` → `.agentignore`. Note there are [open reports](https://github.com/google-antigravity/antigravity-cli/issues/309) that the Antigravity CLI doesn't always fully respect this file in practice. - **OpenCode**: has no native ignore-file support yet. The closest option is the community [`opencode-ignore`](https://github.com/lgladysz/opencode-ignore) plugin, which you install via `opencode.json` and which reads a `.ignore` file. If you adopt it, symlink `.ignore` to `.agentignore` the same way. -- **Claude Code**: has **no** `.claudeignore` (or any other external ignore-file) mechanism — a symlink here would be inert. It automatically respects `.gitignore`. For checked-in paths it can't reach that way (e.g. lock files, vendored code), Claude Code supports `Read` deny rules in `.claude/settings.json` — see the [large-codebases guide](https://code.claude.com/docs/en/large-codebases.md#block-reads-of-generated-and-vendored-code). This repo ships a generated `.claude/settings.json` (tracked in git, like Claude Code's own convention for shared project settings — only `.claude/settings.local.json` is gitignored) so it's enforced out of the box. +- **Claude Code**: has **no** `.claudeignore` (or any other external ignore-file) mechanism — a symlink here would be inert. It automatically respects `.gitignore`. For checked-in paths it can't reach that way (e.g. lock files, vendored code), Claude Code supports `Read` deny rules in `.claude/settings.json` — see the [large-codebases guide](https://code.claude.com/docs/en/large-codebases.md#block-reads-of-generated-and-vendored-code). This repo ships a pre-generated `.claude/settings.json` (tracked in git, like Claude Code's own convention for shared project settings — only `.claude/settings.local.json` is gitignored) so it's enforced out of the box. -To update the pattern list, edit `.agentignore` — the symlinked files pick up the change automatically, and `just format` (or `just sync-agent-ignore` directly) regenerates `.claude/settings.json`'s deny rules from it via `scripts/sync-agent-ignore.sh`, so the two never drift. `just validate` fails if `.claude/settings.json` is stale. +To update the pattern list, edit `.agentignore` — the symlinked files pick up the change automatically. For Claude Code, add corresponding `Read(...)` deny rules directly to `.claude/settings.json` (or pull template updates via `copier update`). ### Upstream Template Updates This project was scaffolded with [Copier](https://copier.readthedocs.io/). To pull in upstream updates (tooling, hooks, CI improvements) from `project-template`: diff --git a/template/scripts/sync-agent-ignore.sh b/template/scripts/sync-agent-ignore.sh deleted file mode 100755 index 08800f1..0000000 --- a/template/scripts/sync-agent-ignore.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# Regenerates .claude/settings.json's Read-deny rules from .agentignore, so -# Claude Code (which has no external ignore-file mechanism) enforces the same -# patterns as the gitignore-syntax .agentignore without hand-duplicating them. -# -# Usage: -# scripts/sync-agent-ignore.sh # regenerate .claude/settings.json -# scripts/sync-agent-ignore.sh --check # verify it's already in sync (no write) -set -euo pipefail - -cd "$(dirname "$0")/.." - -if ! command -v jq >/dev/null 2>&1; then - echo "error: required command 'jq' is not installed or not in PATH" >&2 - echo "tip: run inside 'nix-shell' or install jq via your system package manager" >&2 - exit 1 -fi - -deny=() -while IFS= read -r line || [[ -n "$line" ]]; do - line="${line%$'\r'}" - [[ -z "$line" || "$line" == \#* ]] && continue - if [[ "$line" == */ ]]; then - deny+=("Read(./**/${line%/}/**)") - else - deny+=("Read(./**/${line})") - fi -done < .agentignore - -generated="$(printf '%s\n' "${deny[@]}" | jq -R . | jq -s '{permissions: {deny: .}}')" - -if [[ "${1:-}" == "--check" ]]; then - current="$(cat .claude/settings.json 2>/dev/null || true)" - if [[ "$current" != "$generated" ]]; then - echo "error: .claude/settings.json is stale — run 'just sync-agent-ignore' to regenerate" >&2 - exit 1 - fi - echo ".claude/settings.json is in sync with .agentignore" -else - mkdir -p .claude - printf '%s\n' "$generated" > .claude/settings.json - echo "Regenerated .claude/settings.json from .agentignore" -fi