Recipes

Enforce Claude Code project conventions via hooks, permissions, and path-scoped rules

Discovered from README cross-links · today

Software & DevOpsWork & Operations

Turned loose CLAUDE.md conventions into enforced config for a Python repo.

What they did

The author audited an existing Claude Code setup in a Python repo (CLAUDE.md, /do-issue and /auto-issues commands, a ruff-format PostToolUse hook, CI) and found that most conventions were only prose Claude could ignore. They wrote a detailed ticket cataloguing verified gaps: pyproject.toml had no [tool.ruff] block (so no import sorting or bugbear rules and no line length), a dead [tool.isort] profile, mypy installed but unconfigured and never run, pytest missing as a dependency (every command needed --with pytest), no [tool.pytest.ini_options], and .vscode/settings.json pointing at unittest instead of pytest. A pre-commit git hook was installed with no .pre-commit-config.yaml, forcing PRE_COMMIT_ALLOW_NO_CONFIG=1 on every commit. CLAUDE.md said never push to main but nothing enforced it. Only one hook existed, .env was unprotected, and .claude/settings.json had no permissions block. Area knowledge lived in .claude/preflight.md, readable only by /auto-issues. The proposed fix: add [tool.ruff] with target-version py311, a line length matching current code, and lint.select E/F/I/B/UP; drop isort; decide mypy's fate; add pytest to the dev group with pytest ini options and fix VS Code; add .pre-commit-config.yaml pinned to uv.lock ruff versions; add PreToolUse hooks blocking pushes to main, --force, --no-verify, and .env reads; extend PostToolUse to run ruff check --fix and report errors back; add allow/deny permission lists; and create path-scoped .claude/rules/ files for server/agent, client, and testing. They also flagged hygiene: duplicate grill-me/grilling skills and an untracked .claude/launch.json, and suggested splitting the work into four PRs.

How it went

Delivered as a scoped, checklist-style ticket with acceptance criteria rather than merged code: ruff check/format and all three pytest suites must pass locally and in CI, plain git commit must work without the env workaround, Claude sessions must visibly block main pushes, force-pushes, and .env reads, and rules must load only for their paths (verified via /memory). The author explicitly split the work into four PRs (style+pre-commit, testing+rules, hooks+permissions, rules+preflight cleanup) and listed coverage gates, e2e in CI, and .mcp.json as out of scope.

Worth knowing

This is a plan, not a verified implementation — none of the acceptance criteria are shown passing. Line length and the mypy keep-or-drop decision are left open. The Stop hook is optional and deliberately skips tests. Coverage gates, e2e in CI (needs an OpenAI key secret), and project .mcp.json are deferred. Hook behavior depends on Claude Code's PreToolUse exit-code semantics and permission syntax matching your version.

Wiring

Order: (1) pyproject.toml — add [tool.ruff] (target-version py311, line-length, lint.select E/F/I/B/UP), remove [tool.isort], decide mypy; (2) add pytest to the dev dependency group plus [tool.pytest.ini_options] with testpaths and pythonpath so imports resolve from repo root, then strip --with pytest from CLAUDE.md, CI, and preflight; fix .vscode/settings.json; (3) add .pre-commit-config.yaml with ruff and ruff-format at the same versions as uv.lock so PRE_COMMIT_ALLOW_NO_CONFIG can go; (4) .claude/settings.json — PreToolUse matcher Bash (block push to main, --force, --no-verify, exit 2), PreToolUse matcher Read|Edit|Write (block .env, allow .env.example), PostToolUse running ruff format then ruff check --fix on edited .py files with errors surfaced to Claude, optional Stop hook running ruff check . only; (5) permissions block with allow (uv run ruff *, uv run * pytest *, make e2e, gh issue/pr view *, git status/diff/log) and deny (Read(./.env), Bash(git push --force*), Bash(rm -rf *)); (6) .claude/rules/*.md with paths: frontmatter scoping server-agent.md to server/agent/**, client.md to client/**, testing.md to **/tests/** and e2e/**; move general facts out of .claude/preflight.md into CLAUDE.md or rules. Verify with /memory that rules load only for their paths.

Try it yourself

Audit this repo's Claude Code setup and turn conventions into enforced config. Check: (1) pyproject.toml for [tool.ruff] (target-version, line-length matching current code, lint.select E,F,I,B,UP), dead [tool.isort], and unconfigured mypy; (2) whether pytest is a real dev dependency with [tool.pytest.ini_options] and testpaths/pythonpath, and whether .vscode/settings.json uses pytest; (3) whether a pre-commit hook is installed without .pre-commit-config.yaml; (4) .claude/settings.json for missing permissions and hooks. Then propose: .pre-commit-config.yaml with ruff + ruff-format pinned to uv.lock versions; PreToolUse hooks on Bash blocking git push to main, --force, and --no-verify (exit 2 with a reason); PreToolUse on Read|Edit|Write blocking .env but allowing .env.example; PostToolUse running ruff check --fix after ruff format and reporting errors back; allow/deny permission lists; and path-scoped .claude/rules/*.md with paths: frontmatter for server/agent, client, and testing. Keep CLAUDE.md under ~60 lines. Split into separate PRs and list acceptance criteria.

Source

Tools in this stack