From abe872409db28daaacb57473eb0dbd92ffdf48ce Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 2 Oct 2026 12:50:37 -0700 Subject: [PATCH 1/2] feat(standards): warn when .claude/ is not ignored Advances smartwatermelon/dev-env#178 --- standards/check-claude-ignore.sh | 49 +++++++++++++++++++++++++++ standards/run-standards.sh | 8 +++++ tests/test-check-claude-ignore.sh | 56 +++++++++++++++++++++++++++++++ tests/test-run-standards.sh | 3 ++ 4 files changed, 116 insertions(+) create mode 100755 standards/check-claude-ignore.sh create mode 100755 tests/test-check-claude-ignore.sh diff --git a/standards/check-claude-ignore.sh b/standards/check-claude-ignore.sh new file mode 100755 index 0000000..40ce04b --- /dev/null +++ b/standards/check-claude-ignore.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# check-claude-ignore.sh : warn-only; see dev-env#178. +set -euo pipefail + +repo="${1:?usage: check-claude-ignore.sh }" +probe=".claude/__standards_probe__" +ref="smartwatermelon/dev-env#178" +fix="Add '.claude/' to .gitignore (or '.claude/*' plus '!.claude/' for shared files)," +fix+=" then 'git rm --cached' any tracked file listed. See ${ref}." + +# Blank global excludes. .git/info/exclude still applies. +_git() { git -C "${repo}" -c core.excludesFile=/dev/null "$@"; } + +# Encode %, CR, LF for annotations. +_esc() { + local s="$1" + s="${s//'%'/%25}" + s="${s//$'\r'/%0D}" + s="${s//$'\n'/%0A}" + printf '%s' "${s}" +} + +if ! git -C "${repo}" rev-parse --git-dir >/dev/null 2>&1; then + echo "::error::${repo} is not a git repository" + exit 2 +fi + +warned=0 + +# check-ignore: 0 ignored, 1 not, else error. +rc=0 +_git check-ignore -q --no-index -- "${probe}" || rc=$? +if ((rc == 1)); then + echo "::warning title=claude-ignore::$(_esc ".claude/ is not ignored by the committed .gitignore. ${fix}")" + warned=1 +elif ((rc != 0)); then + echo "::error::git check-ignore failed (exit ${rc})" + exit 2 +fi + +# Tracked files matching an ignore rule. +tracked="$(_git ls-files -ci --exclude-standard -- .claude/)" +if [[ -n "${tracked}" ]]; then + echo "::warning title=claude-ignore::$(_esc "Tracked file(s) under .claude/ match an ignore rule:"$'\n'"${tracked}"$'\n'"${fix}")" + warned=1 +fi + +if ((warned == 0)); then echo ".claude/ ignore policy: OK"; fi +exit 0 diff --git a/standards/run-standards.sh b/standards/run-standards.sh index 8cfbf4e..c606931 100755 --- a/standards/run-standards.sh +++ b/standards/run-standards.sh @@ -16,6 +16,8 @@ # opened fails their unrelated change. Measured across the fleet, that was the # single largest source of standards-check failures. # +# claude-ignore: warn-only, whole-repo. +# # Scope: the flag narrows the four linters that enumerate through _tracked # (shellcheck, yamllint, zizmor, markdownlint). actionlint and the Node-floor # check find their own inputs and stay whole-repo — both are cheap, and @@ -293,6 +295,12 @@ if _skipped node-floor; then echo "== node-floor: skipped by input"; else _lint node-floor bash "${config_dir}/check-node-floor.sh" "${repo}" "${node_floor}" fi +# claude-ignore: warning-only (dev-env#178); never adds to failed[]. +if _skipped claude-ignore; then echo "== claude-ignore: skipped by input"; else + _header claude-ignore + bash "${config_dir}/check-claude-ignore.sh" "${repo}" +fi + echo if ((${#failed[@]} > 0)); then _write_summary diff --git a/tests/test-check-claude-ignore.sh b/tests/test-check-claude-ignore.sh new file mode 100755 index 0000000..b32819b --- /dev/null +++ b/tests/test-check-claude-ignore.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Validation for standards/check-claude-ignore.sh on throwaway git repos. +# It only warns, so cases assert on output and exit 0. +set -euo pipefail +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +checker="${here}/../standards/check-claude-ignore.sh" +# Isolate from the developer's global git config (hooks, excludes). +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null +tmp="$(mktemp -d)" +trap 'rm -rf "${tmp}"' EXIT +pass=0; fail=0 +_ok() { echo " ok $1"; pass=$((pass + 1)); } +_bad() { echo " FAIL $1"; fail=$((fail + 1)); } + +# _repo NAME GITIGNORE-CONTENT [TRACKED-FILE...]: build a fixture repo. +_repo() { + local name="$1" ign="$2" d f + shift 2 + d="${tmp}/${name}" + git init -q "${d}" + if [[ -n "${ign}" ]]; then printf '%b' "${ign}" >"${d}/.gitignore"; fi + mkdir -p "${d}/.claude" + for f in "$@"; do echo x >"${d}/${f}"; done + git -C "${d}" add -f -A +} + +# _run NAME: run the checker; sets out and requires exit 0. +_run() { + out="$("${checker}" "${tmp}/$1" 2>&1)" || { _bad "$1: exit status nonzero"; return 1; } +} +_warns() { grep -q '^::warning title=claude-ignore::' <<<"${out}"; } + +_repo nogi "" +_run nogi && { if _warns; then _ok "no .gitignore warns"; else _bad "no .gitignore did not warn"; fi; } + +_repo clean '.claude/\n' +_run clean && { if _warns; then _bad "'.claude/' ignored warned"; else _ok "'.claude/' ignored is clean"; fi; } + +_repo shared '.claude/*\n!.claude/pre-launch.sh\n' .claude/pre-launch.sh +_run shared && { if _warns; then _bad "negated shared file warned"; else _ok "'.claude/*' + negation + tracked shared file is clean"; fi; } + +_repo tracked '.claude/\n' .claude/README.md +_run tracked && { if _warns && grep -q '\.claude/README\.md' <<<"${out}"; then _ok "tracked file under ignored .claude/ warns and is named"; else _bad "tracked file case missing warning or name"; fi; } + +_repo nonegate '.claude/*\n' .claude/pre-launch.sh +_run nonegate && { if _warns && grep -q '\.claude/pre-launch\.sh' <<<"${out}"; then _ok "'.claude/*' without negation warns and names file"; else _bad "no-negation case missing warning or name"; fi; } + +# A global excludes file must not mask a missing rule. +_repo masked "" +printf '.claude/\n' >"${tmp}/global-ignore" +printf '[core]\n\texcludesFile = %s\n' "${tmp}/global-ignore" >"${tmp}/gitconfig" +out="$(GIT_CONFIG_GLOBAL="${tmp}/gitconfig" "${checker}" "${tmp}/masked" 2>&1)" +if _warns; then _ok "global excludes does not mask a missing rule"; else _bad "global excludes masked the check"; fi + +echo "${pass} passed, ${fail} failed" +[[ "${fail}" -eq 0 ]] diff --git a/tests/test-run-standards.sh b/tests/test-run-standards.sh index 6e94c27..2735c28 100755 --- a/tests/test-run-standards.sh +++ b/tests/test-run-standards.sh @@ -113,6 +113,9 @@ printf 'on: push\npermissions:\n contents: read\njobs:\n a:\n runs-on: ubun echo "lts/krypton" >"${tmp}/clean/.nvmrc" git -C "${tmp}/clean" add -A _expect_pass "clean repo passes every linter" clean +# claude-ignore (dev-env#178) is warning-only: the clean fixture has no +# .gitignore, so the run must warn AND still pass. +if grep -q '^::warning title=claude-ignore::' "${tmp}/clean.log"; then _ok "claude-ignore warns without failing the run"; else _bad "claude-ignore warning missing from the clean run"; fi if bash "${runner}" --repo "${tmp}/bad-sh" --config-dir "${cfg}" --skip shellcheck >/dev/null 2>&1; then _ok "--skip shellcheck disables the linter"; else _bad "--skip shellcheck did not disable it"; fi From b27749facdd320fef3b3312ad5572ac2d36b6eb8 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 2 Oct 2026 12:51:44 -0700 Subject: [PATCH 2/2] fix(standards): git errors in claude-ignore warn instead of failing Advances smartwatermelon/dev-env#178 --- standards/check-claude-ignore.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/standards/check-claude-ignore.sh b/standards/check-claude-ignore.sh index 40ce04b..d29a50b 100755 --- a/standards/check-claude-ignore.sh +++ b/standards/check-claude-ignore.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# check-claude-ignore.sh : warn-only; see dev-env#178. +# check-claude-ignore.sh : warn-only (never fails); see dev-env#178. set -euo pipefail repo="${1:?usage: check-claude-ignore.sh }" @@ -34,12 +34,17 @@ if ((rc == 1)); then echo "::warning title=claude-ignore::$(_esc ".claude/ is not ignored by the committed .gitignore. ${fix}")" warned=1 elif ((rc != 0)); then - echo "::error::git check-ignore failed (exit ${rc})" - exit 2 + echo "::warning title=claude-ignore::git check-ignore failed (exit ${rc}); not checked" + exit 0 fi # Tracked files matching an ignore rule. -tracked="$(_git ls-files -ci --exclude-standard -- .claude/)" +lsrc=0 +tracked="$(_git ls-files -ci --exclude-standard -- .claude/)" || lsrc=$? +if ((lsrc != 0)); then + echo "::warning title=claude-ignore::git ls-files failed (exit ${lsrc}); tracked files not checked" + exit 0 +fi if [[ -n "${tracked}" ]]; then echo "::warning title=claude-ignore::$(_esc "Tracked file(s) under .claude/ match an ignore rule:"$'\n'"${tracked}"$'\n'"${fix}")" warned=1