Anti-Pattern Detection Hooks
Six hooks that detect and prevent common code anti-patterns. Each runs on PreToolUse:Write/Edit events and provides actionable fix suggestions.
Hook Summary
| Hook | Pattern Detected | Severity | Languages |
|---|---|---|---|
| suppress-custom-retry.sh | Custom retry loops when tenacity available | WARNING | Python |
| suppress-v2-files.sh | _v2, _new, _old file naming | ERROR | All |
| suppress-hardcoded-strings.sh | Hardcoded provider/model/URL strings | WARNING | Python, TS, Go |
| suppress-print-statements.sh | print()/console.log when structured logger available | WARNING | Python, TS, Go |
| suppress-isolated-classes.sh | God classes (>15 methods or >300 lines) | WARNING | Python, TS |
| suppress-direct-http.sh | Direct HTTP calls without client abstraction | WARNING | Python, TS, Go |
Parpour-Specific Anti-Patterns
Don't add specs to venture/ subdirectory — specs live at parpour root
Pattern: Adding specification files inside venture/ or other subdirectories.
Why blocked: Specs are canonical at the parpour root level. Subdirectories like venture/ are for implementation, not specification.
Fix:
# Wrong
venture/TECHNICAL_SPEC.md
venture/TRACK_A.md
# Correct
TECHNICAL_SPEC.md
TRACK_A.md
TRACK_B.mdDon't create SaaS-dependent artifact flows — use headless compiler IR
Pattern: Building artifact generation flows that depend on external SaaS services (e.g., OpenAI API, hosted databases).
Why blocked: Parpour must be runnable standalone. All artifact generation should use the headless compiler IR and local processing.
Fix:
# Wrong — depends on external API
def generate_civ_spec():
response = openai.ChatCompletion.create(model="gpt-4", messages=...)
return response.choices[0].text
# Correct — uses headless IR
from parpour.compiler import HeadlessCompiler
def generate_civ_spec():
compiler = HeadlessCompiler()
ir = compiler.parse_config(config)
return compiler.generate_artifact(ir, 'spec')Don't add fallbacks or silent failure paths
Pattern: Adding try/except blocks that hide errors, or feature flags that silently degrade functionality.
Why blocked: Following global CLAUDE instructions — code should fail fast and fail loudly.
Fix:
# Wrong — silent fallback
try:
spec = load_spec(path)
except Exception:
spec = default_spec # Hidden failure
# Correct — fail loudly
spec = load_spec(path) # Raises if not foundHook Details
suppress-custom-retry.sh
What it detects: Hand-rolled retry/backoff loops in Python when tenacity is declared in project dependencies.
Patterns caught:
for attempt in range(N)withsleep+try/exceptwhile Trueretry loops with attempt counters- Manual exponential backoff (
2 ** attempt)
Fix:
# Before (anti-pattern)
for attempt in range(5):
try:
result = httpx.get(url)
break
except Exception:
time.sleep(2 ** attempt)
# After (correct)
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential())
def fetch(url: str) -> httpx.Response:
return httpx.get(url, timeout=10)suppress-v2-files.sh
What it detects: Files with _v2, _new, _old, _backup, _copy, _temp suffixes.
Why blocked (ERROR): v2 files lead to:
- Import confusion (which version to use?)
- Stale code copies that diverge
- No clear migration path
Fix options:
- Modify the original file directly
- Use feature flags for behavioral changes
- Use interface versioning (APIv2 endpoint, not handler_v2.py)
- If migrating, rename the original and update all imports in one commit
suppress-hardcoded-strings.sh
What it detects: Hardcoded provider names, model identifiers, and API URLs in source code.
Patterns caught:
- LLM model names:
"gpt-4","claude-3","gemini-1.5" - API URLs:
"https://api.openai.com/..." - Provider identifiers:
"aws","openai","anthropic"as string literals
Excludes: Test files, imports, comments.
Fix:
# Before
response = client.chat("gpt-4", messages)
# After
from config import settings
response = client.chat(settings.default_model, messages)suppress-print-statements.sh
What it detects: Unstructured logging (print, console.log, fmt.Println) when a structured logging library is in project dependencies.
Dependency triggers:
- Python:
structlogin deps -> blocksprint()andlogging.getLogger() - TypeScript:
pino/winstonin deps -> blocksconsole.log() - Go:
zerolog/zapin deps -> blocksfmt.Println()
Fix:
# Before
print(f"Processing user {user_id}")
# After
import structlog
log = structlog.get_logger()
log.info("processing_user", user_id=user_id)suppress-isolated-classes.sh
What it detects: Classes that are too large ("God classes").
Thresholds:
- More than 15 methods
- More than 300 lines
Fix:
- SRP decomposition: Extract cohesive method groups into separate classes
- Composition: Break into composed components instead of one monolith
- Strategy pattern: Extract behavioral variations into strategy classes
- DTO extraction: Move data-only methods into separate dataclasses
suppress-direct-http.sh
What it detects: Direct HTTP client calls (requests.get, fetch(), http.Get) in business logic.
Why: Direct calls scatter URL construction, authentication, retry logic, timeout handling, and error mapping across the codebase.
Excludes: Files in clients/, adapters/, infrastructure/, transport/ directories. Files named *client*, *http*, *fetcher*.
Fix:
# Before (scattered in business logic)
response = requests.get(f"{BASE_URL}/users/{user_id}", headers=auth_headers)
# After (centralized client)
class UserClient:
def __init__(self, base_url: str, auth: Auth):
self.client = httpx.AsyncClient(base_url=base_url, auth=auth)
async def get_user(self, user_id: str) -> User:
response = await self.client.get(f"/users/{user_id}")
response.raise_for_status()
return User.model_validate(response.json())Integration
Claude Code Hooks (settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
"hooks/suppress-custom-retry.sh $FILE_PATH",
"hooks/suppress-v2-files.sh $FILE_PATH",
"hooks/suppress-hardcoded-strings.sh $FILE_PATH",
"hooks/suppress-print-statements.sh $FILE_PATH",
"hooks/suppress-isolated-classes.sh $FILE_PATH",
"hooks/suppress-direct-http.sh $FILE_PATH"
]
}
]
}
}Pre-commit (local hooks)
- repo: local
hooks:
- id: no-v2-files
name: Block v2/new/old file creation
entry: hooks/suppress-v2-files.sh
language: script
stages: [pre-commit]Testing Hooks
Each hook can be tested standalone:
# Test v2 file detection (should print error and exit 1)
echo "test" > /tmp/handler_v2.py
./hooks/suppress-v2-files.sh /tmp/handler_v2.py
echo "Exit code: $?"
# Test custom retry detection (should print warning)
cat > /tmp/retry_test.py << 'EOF'
for attempt in range(5):
try:
result = httpx.get(url)
break
except Exception:
time.sleep(2 ** attempt)
EOF
./hooks/suppress-custom-retry.sh /tmp/retry_test.py