Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion standards/run-standards.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,18 @@ if _skipped shellcheck; then echo "== shellcheck: skipped by input"; else
esac
done < <(_tracked || true)
if ((${#files[@]} == 0)); then echo "::notice::no shell files"; else
(cd "${repo}" && shellcheck -S info "${files[@]}") || _fail shellcheck
# Config precedence, as for the other linters: a repo-root .shellcheckrc
# wins, else the canonical file here. Passing it explicitly matters more
# than elsewhere -- shellcheck searches ANCESTOR directories, so a bare
# invocation under a checkout whose parent carries a .shellcheckrc picks
# up settings CI never intended (claude-config#534).
#
# Note these do not merge: --rcfile replaces the search, and a repo-root
# file halts it. A repo-local config therefore restates the whole policy.
cfg=""
if [[ -f "${repo}/.shellcheckrc" ]]; then cfg="${repo}/.shellcheckrc"; fi
[[ -n "${cfg}" ]] || cfg="${config_dir}/shellcheckrc"
(cd "${repo}" && shellcheck --rcfile "${cfg}" -S info "${files[@]}") || _fail shellcheck
fi
fi

Expand Down
38 changes: 38 additions & 0 deletions standards/shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Canonical shellcheck config for standards-check.yml.
#
# shellcheck was the only linter in run-standards.sh with no canonical config:
# yamllint and markdownlint ship one here, while shellcheck ran bare and
# resolved its config from whatever happened to be on the machine. Because
# shellcheck searches ancestor directories, a local run under a checkout whose
# parent carries a ~/.shellcheckrc silently picked up settings CI never saw --
# 68 findings locally against CI's 24 on the same tree.
# See twistedmelonman/claude-config#534.
#
# Measured 2026-09-18 across every clone with shell files: this config takes
# 21 of 24 repos to zero findings, including all three that previously carried
# their own .shellcheckrc. Those three are removed in the same change.
#
# external-sources + source-path resolve files sourced through a variable path
# (`source "${LIB_DIR}/foo.sh"`) instead of silencing SC1091. Resolving means
# the sourced code is analyzed; silencing means it is skipped. source-path
# SCRIPTDIR additionally maps sources written against a deployed layout back to
# the repo layout, where those files sit beside the sourcing script.
#
# SC2310 (function invoked in a conditional, disabling set -e) is informational
# and fires throughout normal guard-clause style. It is disabled fleet-wide
# rather than per-repo so no repo needs its own config to silence it.
#
# `enable=all` is deliberately NOT set here. Measured, it takes four otherwise
# clean repos to 124+ combined findings (dotfiles 63, vpn-lan-bridge 12,
# claude-wrapper 10, scripts 3, LaunchAgents 1). A base that forces per-repo
# exceptions is not a base. It stays in ~/.shellcheckrc as the stricter
# interactive config: local advisory, CI enforcing.
#
# NOTE: shellcheck config files do NOT merge. A repo-root .shellcheckrc halts
# the search entirely and --rcfile replaces it, so a per-repo exception must
# restate this whole file and silently drifts when this one changes. Keep
# exceptions rare, and prefer a file-level `# shellcheck disable` with a
# comment over a repo-level config.
external-sources=true
source-path=SCRIPTDIR
disable=SC2310
Loading