| name | config-hygiene |
| description | Fixes configuration hygiene issues including gitignore patterns, ESLint config duplication, and hook scripts. Use when encountering backup files in repo, missing gitignore patterns, or config maintenance issues. |
Configuration Hygiene Fixes
Fixes for project configuration issues. Clean configs prevent noise, ensure consistency, and improve developer experience.
Quick Start
- Identify the config issue (gitignore, ESLint, husky)
- Apply the recommended fix
- Verify no unintended side effects
- Commit config changes
Priority Matrix
| Issue | Priority | Impact |
|---|---|---|
| Backup files in repo | P2 | Noise, confusion |
| Missing gitignore patterns | P2 | Unwanted files tracked |
| Husky silent failure | P2 | Hidden hook failures |
| ESLint config duplication | P3 | Maintenance burden |
| Permissive lint threshold | P2 | Quality regression |
Workflows
Backup/Artifact Files in Repository (#35 - 12 occurrences)
Detection: Files matching *.bak, *.orig, *~ patterns.
Pattern: Editor/merge artifacts committed to repo.
Fix Strategy: Remove from git, add to gitignore.
# Remove from git (keep locally)
git rm --cached "*.bak"
git rm --cached "*.orig"
git rm --cached "*~"
# Add to .gitignore
echo "*.bak" >> .gitignore
echo "*.orig" >> .gitignore
echo "*~" >> .gitignore
# Commit
git add .gitignore
git commit -m "chore: ignore backup/artifact files"
Missing .gitignore Patterns (#36 - 8 occurrences)
Detection: Common patterns not in .gitignore.
Fix Strategy: Add comprehensive patterns.
# Editor artifacts
*.swp
*.swo
*~
*.bak
*.orig
# OS files
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.sublime-*
# Build outputs
dist/
build/
*.tsbuildinfo
# Dependencies
node_modules/
# Environment
.env
.env.*
!.env.example
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Test coverage
coverage/
.nyc_output/
# Cache
.cache/
.eslintcache
.parcel-cache/
Template: See templates/gitignore-node.txt for complete Node.js template.
Husky Prepare Script Silent Failure (#37 - 1 occurrence)
Detection: prepare script uses || true fallback.
Pattern: Hook installation fails silently.
// PROBLEM - hides failures
{
"scripts": {
"prepare": "husky install || true"
}
}
Fix Strategy 1: Conditional execution based on CI.
// SOLUTION - check CI environment
{
"scripts": {
"prepare": "node -e \"if (process.env.CI !== 'true') require('husky').install()\""
}
}
Fix Strategy 2: Use is-ci package.
npm install --save-dev is-ci
{
"scripts": {
"prepare": "is-ci || husky install"
}
}
Fix Strategy 3: Husky v9+ auto-setup.
// Husky v9 uses .husky/_/husky.sh automatically
{
"scripts": {
"prepare": "husky"
}
}
Permissive lint:strict Threshold (#38 - 1 occurrence)
Detection: High --max-warnings value in lint script.
Pattern: Too many warnings allowed.
// PROBLEM - too permissive
{
"scripts": {
"lint:strict": "eslint . --max-warnings 500"
}
}
Fix Strategy: Progressive reduction.
// Week 1: Lower to current count + small buffer
{
"scripts": {
"lint:strict": "eslint . --max-warnings 350"
}
}
// Week 2: Lower further
{
"scripts": {
"lint:strict": "eslint . --max-warnings 200"
}
}
// Target: Zero tolerance
{
"scripts": {
"lint:strict": "eslint . --max-warnings 0"
}
}
Tracking Progress:
# Get current warning count
npx eslint . 2>&1 | tail -1
# Output: "X warnings" or "X problems (Y errors, Z warnings)"
ESLint Config Duplication (#39, #40)
Issue #39: Repeated plugin/globals definitions.
// PROBLEM - repeated in multiple blocks
export default [
{
files: ['**/*.ts'],
plugins: { '@typescript-eslint': tsPlugin },
},
{
files: ['**/*.tsx'],
plugins: { '@typescript-eslint': tsPlugin }, // Duplicated
},
];
Fix Strategy: Extract shared config.
// SOLUTION - shared base config
const baseTypescriptConfig = {
plugins: {
'@typescript-eslint': tsPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
},
},
};
export default [
{
files: ['**/*.ts'],
...baseTypescriptConfig,
rules: { /* ts-specific rules */ },
},
{
files: ['**/*.tsx'],
...baseTypescriptConfig,
rules: { /* tsx-specific rules */ },
},
];
Issue #40: Disabled rules duplicated across blocks.
// PROBLEM - duplicated disabled rules
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
},
},
Fix Strategy: Extract to named constant with documentation.
// SOLUTION - extracted with docs
/**
* Rules disabled during migration to strict TypeScript.
* TODO: Re-enable progressively - see issue #456
*/
const migrationDisabledRules = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
} as const;
export default [
{
files: ['**/*.ts'],
rules: {
...migrationDisabledRules,
// ts-specific rules
},
},
];
Recursive Directory Walking Without Limits (#23 - 1 occurrence)
Pattern: Unbounded recursion in file walking.
// PROBLEM - no limits
async function walkDir(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
if (entry.isDirectory()) {
files.push(...await walkDir(path.join(dir, entry.name))); // No limit!
}
}
return files;
}
Fix Strategy: Add depth limit and visited tracking.
// SOLUTION - with limits
interface WalkOptions {
maxDepth?: number;
maxFiles?: number;
}
async function walkDir(
dir: string,
options: WalkOptions = {},
depth = 0,
visited = new Set<string>()
): Promise<string[]> {
const { maxDepth = 10, maxFiles = 10000 } = options;
if (depth > maxDepth) {
return [];
}
const realPath = await fs.realpath(dir);
if (visited.has(realPath)) {
return []; // Circular symlink
}
visited.add(realPath);
const entries = await fs.readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
if (files.length >= maxFiles) break;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await walkDir(fullPath, options, depth + 1, visited));
} else {
files.push(fullPath);
}
}
return files;
}
Regex Created Per-Match (#24 - 1 occurrence)
Pattern: Regex compiled on every function call.
// PROBLEM - regex created every call
function containsKeyword(text: string, keyword: string): boolean {
const regex = new RegExp(`\\b${keyword}\\b`, 'i');
return regex.test(text);
}
Fix Strategy: Pre-compile and cache.
// SOLUTION - cached regex
const keywordRegexCache = new Map<string, RegExp>();
function getKeywordRegex(keyword: string): RegExp {
let regex = keywordRegexCache.get(keyword);
if (!regex) {
regex = new RegExp(`\\b${escapeRegex(keyword)}\\b`, 'i');
keywordRegexCache.set(keyword, regex);
}
return regex;
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function containsKeyword(text: string, keyword: string): boolean {
return getKeywordRegex(keyword).test(text);
}
Scripts
Check Config Hygiene
node scripts/check-config.js
Fix Gitignore
node scripts/fix-gitignore.js
Templates
Node.js .gitignore
See templates/gitignore-node.txt
ESLint Base Config
See templates/eslint-base.js
CI/CD Considerations
Ensuring Hooks Run
# GitHub Actions
- name: Install dependencies
run: npm ci
# This runs "prepare" script which installs husky
# Make sure CI=true is set to skip if needed
Lint in CI
- name: Lint
run: npm run lint:strict
# Fails if warnings exceed threshold
Progressive Strictness
# Allow gradual improvement
- name: Check lint progress
run: |
WARNINGS=$(npx eslint . 2>&1 | grep -oP '\d+ warning' | grep -oP '\d+' || echo "0")
THRESHOLD=200
if [ "$WARNINGS" -gt "$THRESHOLD" ]; then
echo "Too many warnings: $WARNINGS (max: $THRESHOLD)"
exit 1
fi