Help us improve
Share bugs, ideas, or general feedback.
From harness-engineering
Sets up ast-grep in TypeScript codebases with rules detecting anti-patterns, enforcing best practices, and preventing bugs. Creates sgconfig.yml, rule files, and tests for structural linting, legacy bans, and ratchet gates.
npx claudepluginhub alchemiststudiosdotai/harness-engineeringHow this skill is triggered — by the user, by Claude, or both
Slash command
/harness-engineering:ast-grep-setupThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Set up ast-grep with common TypeScript rules for detecting anti-patterns, enforcing best practices, and preventing bugs.
scripts/validate-rule.pytemplates/rule-tests/no-console-log-test.ymltemplates/rule-tests/no-debugger-test.ymltemplates/rule-tests/no-empty-catch-test.ymltemplates/rule-tests/no-floating-promises-test.ymltemplates/rule-tests/no-node-in-frontend-test.ymltemplates/rules/no-console-log.ymltemplates/rules/no-debugger.ymltemplates/rules/no-empty-catch.ymltemplates/rules/no-floating-promises.ymltemplates/rules/no-node-in-frontend.ymltemplates/sgconfig.ymlAnalyzes codebases for anti-patterns, code smells, and quality issues using ast-grep structural pattern matching in JavaScript, TypeScript, Python, Vue, and React. Use for code reviews and technical debt detection.
Sets up strict production-grade ESLint configuration for TypeScript projects and systematically fixes all linting issues via auto-fix and manual remediation.
Guides writing ast-grep rules for AST-based structural code search to find patterns like unhandled async functions or specific constructs beyond text matching.
Share bugs, ideas, or general feedback.
Set up ast-grep with common TypeScript rules for detecting anti-patterns, enforcing best practices, and preventing bugs.
Agents: Before writing any rules, read the YAML Structure Rules section.
The most common mistakes are:
constraints inside rule: (must be at top level)not: blocksall: or any:Use the validation script to catch these before running ast-grep:
python3 scripts/validate-rule.py rules/*.yml
# Create sgconfig.yml in project root
mkdir -p rules/ast-grep/rules rules/ast-grep/rule-tests
cat > rules/ast-grep/sgconfig.yml << 'EOF'
ruleDirs:
- rules
testConfigs:
- testDir: rule-tests
allowedFixers: []
EOF
See the Rule Library below for ready-to-use rules.
# Scan the codebase
cd rules/ast-grep && ast-grep scan
# Run rule tests
cd rules/ast-grep && ast-grep test
# Update test snapshots after rule changes
cd rules/ast-grep && ast-grep test -U
Prevents function parameters without explicit types (implicit any).
# rules/no-implicit-any-params.yml
id: no-implicit-any-params
language: TypeScript
severity: warning
message: "Parameter '$PARAM' lacks explicit type annotation"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: function $FUNC($PARAM) { $$$BODY }
constraints:
PARAM:
not:
has:
kind: type_annotation
Bans direct property access on any typed values.
# rules/no-unsafe-any-usage.yml
id: no-unsafe-any-usage
language: TypeScript
severity: error
message: "Unsafe property access on 'any' type. Cast or add type guard."
rule:
pattern: $EXPR.$PROP
constraints:
EXPR:
typeAnnotation: any
Prevents unhandled promises that could fail silently.
# rules/no-floating-promises.yml
id: no-floating-promises
language: TypeScript
severity: error
message: "Promise is not awaited, returned, or handled with .catch()"
files:
- src/**/*.ts
- src/**/*.tsx
ignores:
- new Promise($$$)
- Promise.$FUNC($$$)
rule:
all:
- pattern: $PROMISE_FUNC($$$ARGS)
- has:
kind: call_expression
pattern: $PROMISE_FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
- not:
inside:
kind: call_expression
pattern: $$$.catch($$$)
stopBy: end
constraints:
PROMISE_FUNC:
regex: (fetch|axios\.[a-z]+|async|\.then)
Detects async function calls without await.
# rules/no-missing-await.yml
id: no-missing-await
language: TypeScript
severity: warning
message: "Async function '$FUNC' called without await"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: $FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
constraints:
FUNC:
typeAnnotation: /^Promise</
Bans empty catch blocks that swallow errors.
# rules/no-empty-catch.yml
id: no-empty-catch
language: TypeScript
severity: error
message: "Empty catch block silently swallows errors. Log or re-throw."
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: try { $$$TRY } catch ($ERROR) { $$$CATCH }
- not:
has:
kind: statement
inside:
kind: catch_clause
pattern: $$$CATCH
#### require-error-logging
Requires error logging in catch blocks.
```yaml
# rules/require-error-logging.yml
id: require-error-logging
language: TypeScript
severity: warning
message: "Catch block should log the error"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: try { $$$TRY } catch ($ERR) { $$$CATCH }
- not:
has:
kind: call_expression
pattern: console.$LOG($ERR)
inside:
kind: catch_clause
pattern: $$$CATCH
- not:
has:
kind: call_expression
pattern: logger.$LOG($ERR)
inside:
kind: catch_clause
pattern: $$$CATCH
Flags useEffect hooks that might be missing dependencies.
# rules/no-use-effect-missing-deps.yml
id: no-use-effect-missing-deps
language: TypeScript
severity: warning
message: "useEffect has an empty dependency array but references external values"
files:
- src/**/*.tsx
rule:
pattern: useEffect($FUNC, [])
constraints:
FUNC:
has:
kind: identifier
pattern: $ID
not:
pattern: console
Prevents direct state mutation in React.
# rules/no-direct-state-mutation.yml
id: no-direct-state-mutation
language: TypeScript
severity: error
message: "Do not mutate state directly. Use the setter function."
files:
- src/**/*.tsx
rule:
all:
- pattern: $STATE.$PROP = $VAL
- has:
kind: identifier
pattern: $STATE
regex: ^set[A-Z]
Warns about using reduce to build objects (often less readable).
# rules/no-array-reduce-for-objects.yml
id: no-array-reduce-for-objects
language: TypeScript
severity: warning
message: "Consider using Object.fromEntries() or a for...of loop instead of reduce for building objects"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: $ARR.reduce(($ACC, $ITEM) => { $$$BODY; return $ACC; }, {})
Prevents regex creation inside loops (compiles on each iteration).
# rules/no-regex-in-loop.yml
id: no-regex-in-loop
language: TypeScript
severity: warning
message: "Creating regex inside loop - move outside or use constant"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
inside:
kind: for_statement
stopBy: end
pattern: /$PAT/
Enforces module boundaries (customize for your architecture).
# rules/no-cross-module-imports.yml
id: no-cross-module-imports
language: TypeScript
severity: error
message: "Domain modules should not import from other domain modules directly"
files:
- src/domain/**/*.ts
rule:
all:
- pattern: import $$$ from "$MOD"
- matches:
source: $MOD
contains: /domain/
Prevents Node.js modules from being imported in frontend code.
# rules/no-node-in-frontend.yml
id: no-node-in-frontend
language: TypeScript
severity: error
message: "Node.js built-in modules cannot be used in frontend code"
files:
- src/frontend/**/*.ts
- src/frontend/**/*.tsx
- src/client/**/*.ts
- src/client/**/*.tsx
rule:
all:
- pattern: import $$$ from "$MOD"
- matches:
source: $MOD
regex: ^(fs|path|os|crypto|http|https|net|dgram|dns|cluster|module|vm|child_process|worker_threads)$
Prevents console.log in production code (use a logger instead).
# rules/no-console-log.yml
id: no-console-log
language: TypeScript
severity: warning
message: "Use a proper logger instead of console.log"
files:
- src/**/*.ts
- src/**/*.tsx
ignores:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/__tests__/**"
rule:
pattern: console.log($$$ARGS)
Prevents debugger statements.
# rules/no-debugger.yml
id: no-debugger
language: TypeScript
severity: error
message: "Remove debugger statement before committing"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
pattern: debugger;
Suggests const when variable is never reassigned.
# rules/prefer-const-over-let.yml
id: prefer-const-over-let
language: TypeScript
severity: hint
message: "Consider using 'const' since this variable is never reassigned"
files:
- src/**/*.ts
- src/**/*.tsx
rule:
all:
- pattern: let $VAR = $INIT
- not:
follows:
pattern: $VAR = $NEWVAL
stopBy: end
Each rule should have a corresponding test file:
id: no-floating-promises
valid:
- |
const result = await fetch('/api/users');
- |
return fetch('/api/users');
- |
fetch('/api/users').catch(err => console.error(err));
- |
new Promise((resolve) => setTimeout(resolve, 1000));
- |
Promise.all([fetch('/a'), fetch('/b')]);
invalid:
- |
function getUsers() {
fetch('/api/users');
}
- |
async function load() {
fetch('/api/data');
}
To implement a ratchet (block new violations while allowing existing ones):
# 1. Generate baseline of current violations
ast-grep scan --json > baseline/violations.json
# 2. Create baseline extractor script
cat > tools/ast-grep/baseline-check.sh << 'SCRIPT'
#!/bin/bash
# Check only new violations against baseline
ast-grep scan --json | node -e '
const baseline = require("./baseline/violations.json");
const current = JSON.parse(require("fs").readFileSync(0, "utf-8"));
const baselineSet = new Set(baseline.map(v => `${v.file}:${v.line}:${v.ruleId}`));
const newViolations = current.filter(v => !baselineSet.has(`${v.file}:${v.line}:${v.ruleId}`));
if (newViolations.length > 0) {
console.error("New violations found:");
newViolations.forEach(v => console.error(`${v.file}:${v.line} - ${v.message}`));
process.exit(1);
}
'
SCRIPT
chmod +x tools/ast-grep/baseline-check.sh
# .github/workflows/ast-grep.yml
name: ast-grep
on: [push, pull_request]
jobs:
ast-grep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
- name: Install ast-grep
run: npm install -g @ast-grep/cli
- name: Run ast-grep scan
run: cd rules/ast-grep && ast-grep scan
- name: Run rule tests
run: cd rules/ast-grep && ast-grep test
VS Code: Install the "ast-grep" extension for inline highlighting.
inside, follows, precedes for contextstopBy: end or stopBy: neighbor| Pattern | Matches |
|---|---|
function $FUNC($$$PARAMS) { $$$BODY } | Function declarations |
const $VAR = $EXPR | Const declarations |
$EXPR.$PROP | Property access |
import $$$ from "$MOD" | Import statements |
export $KIND $NAME | Export declarations |
$FUNC($$$ARGS) | Function calls |
await $EXPR | Await expressions |
try { $$$TRY } catch ($ERR) { $$$CATCH } | Try-catch |
$ARR.map($FN) | Array methods |
STOP: Read this before writing rules. These are the most common mistakes agents make.
constraints Goes at TOP LEVELWRONG - constraints inside rule:
id: bad-example
rule:
pattern: import $NAME from $MOD
constraints: # ❌ WRONG: constraints inside rule
MOD:
regex: "fs"
RIGHT - constraints at top level:
id: good-example
rule:
pattern: import $NAME from $MOD
constraints: # ✓ CORRECT: constraints at root level
MOD:
regex: "fs"
WRONG - duplicate not: keys:
rule:
all:
- pattern: $FUNC($$$ARGS)
- not: # ❌ First not
inside:
kind: await_expression
- not: # ❌ DUPLICATE KEY - YAML will only keep one!
inside:
kind: return_statement
RIGHT - wrap in all: with separate patterns:
rule:
all:
- pattern: $FUNC($$$ARGS)
- not:
inside:
kind: await_expression
stopBy: end
- not:
inside:
kind: return_statement
stopBy: end
Or use any: for alternatives:
rule:
any:
- pattern: fetch($$$)
- pattern: axios.$METHOD($$$)
all: / any: StructurePattern: When you need multiple conditions or multiple negations, always wrap in all: or any:.
# Multiple conditions all must match
rule:
all:
- pattern: $FUNC($$$ARGS)
- has:
kind: call_expression
- not:
inside:
kind: await_expression
# Any of these patterns match
rule:
any:
- pattern: console.log($$$)
- pattern: console.warn($$$)
- pattern: console.error($$$)
id: rule-id # Required: unique identifier
language: TypeScript # Required: target language
severity: error # Required: error, warning, hint, info
message: "Error message" # Required: user-facing message
files: # Optional: glob patterns
- src/**/*.ts
ignores: # Optional: exclusion patterns
- "**/*.test.ts"
rule: # Required: the matching rule
pattern: ... # Basic pattern
# OR
all: [] # Multiple conditions
# OR
any: [] # Alternative patterns
constraints: # Optional: variable constraints (TOP LEVEL!)
VAR_NAME:
regex: "pattern"
utils: # Optional: utility patterns
MY_UTIL:
pattern: ...
Always validate your YAML structure before testing:
# Check YAML is valid
python3 -c "import yaml; yaml.safe_load(open('rules/my-rule.yml'))" && echo "YAML OK"
# Check rule structure
python3 scripts/validate-rule.py rules/my-rule.yml
# Then run ast-grep
cd rules/ast-grep && ast-grep scan
| Error | Cause | Fix |
|---|---|---|
missing field constraints | constraints inside rule | Move to top level |
yaml.scanner.ScannerError | Duplicate keys | Use all: wrapper |
unknown variant | Invalid enum value | Check docs for valid values |
did not find expected key | Indentation error | Check YAML indentation |
Use this script to validate rule files before committing:
#!/usr/bin/env python3
"""Validate ast-grep rule YAML structure."""
import yaml
import sys
from pathlib import Path
def validate_rule(file_path):
"""Validate a single rule file."""
content = Path(file_path).read_text()
data = yaml.safe_load(content)
errors = []
# Check required fields
required = ['id', 'language', 'severity', 'message', 'rule']
for field in required:
if field not in data:
errors.append(f"Missing required field: {field}")
# Check constraints at wrong level (common mistake)
if 'rule' in data and isinstance(data['rule'], dict):
if 'constraints' in data['rule']:
errors.append("constraints inside rule: - must be at TOP LEVEL")
if 'utils' in data['rule']:
errors.append("utils inside rule: - must be at TOP LEVEL")
if 'transform' in data['rule']:
errors.append("transform inside rule: - must be at TOP LEVEL")
# Check for duplicate keys by analyzing raw YAML
lines = content.split('\n')
for i, line in enumerate(lines, 1):
stripped = line.lstrip()
if stripped.startswith('- '):
continue # Skip list items
if ':' in stripped:
key = stripped.split(':')[0]
# This is a simple check - proper parsing would be more robust
if errors:
print(f"❌ {file_path}")
for err in errors:
print(f" - {err}")
return False
else:
print(f"✓ {file_path}")
return True
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: validate-rule.py <rule-file.yml> [rule-file2.yml ...]")
sys.exit(1)
all_valid = True
for path in sys.argv[1:]:
if not validate_rule(path):
all_valid = False
sys.exit(0 if all_valid else 1)
Save as scripts/validate-rule.py and run:
python3 scripts/validate-rule.py rules/*.yml
Add this to .git/hooks/pre-commit or .pre-commit-config.yaml:
#!/bin/bash
# .git/hooks/pre-commit - validate ast-grep rules
RULES_DIR="rules/ast-grep/rules"
if [ -d "$RULES_DIR" ]; then
echo "Validating ast-grep rules..."
if ! python3 scripts/validate-rule.py "$RULES_DIR"/*.yml; then
echo ""
echo "❌ Rule validation failed. Fix YAML structure errors before committing."
echo " See skills/ast-grep-setup/SKILL.md for YAML structure rules."
exit 1
fi
fi
exit 0
Make it executable:
chmod +x .git/hooks/pre-commit
Add this job to validate rules in CI:
# .github/workflows/validate-ast-grep-rules.yml
name: Validate ast-grep Rules
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pyyaml
run: pip install pyyaml
- name: Validate rule files
run: python3 scripts/validate-rule.py rules/ast-grep/rules/*.yml