How to configure Ruff with Claude Code
Claude Code does not run Ruff by default, so Python edits land unformatted and lint errors slip through unless you wire Ruff in yourself. Out of the box, Claude often calls bare ruff (which may resolve outside the project’s virtual environment) and skips formatting after edits. Wiring Ruff in correctly takes four layers, each with a different job.
This guide covers all four and how they stack together:
- A CLAUDE.md instruction that points Claude at Ruff
- The Astral plugin’s
/astral:ruffskill for on-demand Ruff guidance - A PostToolUse hook that auto-formats every Python edit
- A pre-commit gate that catches anything Claude missed before commit time
Prerequisites
- Claude Code installed
- uv installed
- A Python project with Ruff added as a dev dependency (
uv add --dev ruff) jqinstalled if you want to use the shell version of the auto-format hook
Pick the right layer for your goal
Each layer answers a different question. Pick the ones that match what you actually need; nothing requires installing all four.
| Goal | Layer |
|---|---|
Tell Claude which Ruff commands to prefer and to invoke them through uv run |
CLAUDE.md |
| Give Claude up-to-date Ruff usage guidance on demand (rule selection, migrating from Black or flake8) | /astral:ruff skill |
| Auto-format every Python file Claude writes or edits | PostToolUse hook |
| Catch unfixable lint errors before they reach the repo, even when Claude is not the one committing | pre-commit |
Tell Claude Code to use Ruff with CLAUDE.md
A CLAUDE.md file in your project root is the simplest place to point Claude at Ruff. Claude reads it on every prompt, so a few lines about Ruff commands keep the model from inventing alternatives.
Add this section to your CLAUDE.md:
## Linting and formatting
This project uses Ruff for both linting and formatting. Do not call Black,
flake8, isort, or pylint.
- Lint: `uv run ruff check .`
- Lint and auto-fix: `uv run ruff check --fix .`
- Format: `uv run ruff format .`
- Check formatting without writing: `uv run ruff format --check .`
- Always invoke Ruff through `uv run` so it resolves to the project's
virtual environment.
Ruff configuration lives in `pyproject.toml` under `[tool.ruff]`. Do not
add a separate `ruff.toml` or `.ruff.toml`. Do not add inline `# noqa`
comments without a rule code.Anthropic’s CLAUDE.md best practices recommend a pruning test for every line: would Claude make this mistake without the instruction? Each line above passes that test. Claude defaults to bare ruff and writes bare # noqa comments without rule codes; older training data also nudges it toward Black when asked to format.
Tip
If you want a starter that already includes Ruff alongside uv, pytest, and pre-commit guidance, use the pydevtools CLAUDE.md template instead of writing one from scratch.
For projects that don’t yet have a curated Ruff configuration, see How to configure recommended Ruff defaults; Claude will follow whatever rules pyproject.toml declares.
Add the Astral plugin’s Ruff skill
Astral publishes an official Claude Code plugin that bundles /astral:uv, /astral:ruff, and /astral:ty. The Ruff skill teaches Claude Ruff’s current lint and format workflow, plus migration from older formatter and linter setups. The skill body only loads when invoked, so it doesn’t compete with CLAUDE.md for the always-on context budget.
Inside Claude Code, run:
/plugin marketplace add astral-sh/claude-code-plugins
/plugin install astral@astral-shOnce installed, ask Claude to “format this module with /astral:ruff” or “migrate this Black config to Ruff using /astral:ruff” and it follows Astral’s documented workflow for the tool.
To make Claude reach for the skill automatically when you mention Ruff, add this line to your CLAUDE.md:
When working with Ruff, invoke `/astral:ruff` to follow Astral's
recommended usage.For a team-wide install via .claude/settings.json so every collaborator gets the plugin after trusting the repository, see How to install the Astral plugins for Claude Code.
Auto-format every Python edit with a hook
A CLAUDE.md instruction tells Claude what to do; a hook guarantees it. The PostToolUse hook below runs ruff check --fix and ruff format on every Python file Claude writes or edits, so newly generated code lands correctly formatted whether or not Claude remembered.
Create .claude/hooks/ruff-after-edit.sh in your project:
#!/usr/bin/env bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
if [[ "$file_path" == *.py ]] && [[ -f "$file_path" ]]; then
uv run ruff check --fix --quiet "$file_path" 2>/dev/null
uv run ruff format --quiet "$file_path" 2>/dev/null
fi
exit 0Make it executable:
chmod +x .claude/hooks/ruff-after-edit.shRegister it in .claude/settings.json under PostToolUse with a matcher that fires on Write and Edit (and MultiEdit if your Claude Code build supports it):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ruff-after-edit.sh"
}
]
}
]
}
}The hook runs after Claude’s edit is already on disk, so it can’t undo a bad change. What it can do is normalize formatting and apply Ruff’s auto-fixable rules every time, which keeps diffs clean and avoids “Claude wrote single quotes again” sessions.
Test the hook before relying on it. Create a deliberately-ugly Python file, pipe a sample tool-call payload to the hook, and confirm the file gets formatted:
printf 'x= { "a":1}\n' > example.py
printf '{"tool_input":{"file_path":"%s/example.py"}}\n' "$PWD" \
| bash .claude/hooks/ruff-after-edit.sh
cat example.py # Should show Ruff-formatted outputThe hook reads .tool_input.file_path because Claude Code passes the edited path there for PostToolUse Write and Edit events. In practice, Ruff prints unfixable violations to stdout; the hook only silences stderr, so any diagnostics from ruff check show up directly in your terminal during this test.
Tip
If jq isn’t available everywhere your team works, swap the shell hook for a Python equivalent. The hooks reference at How to write Claude Code hooks for Python projects shows the pattern alongside other Python hooks, including one that blocks bare python and another that runs a type checker on Stop.
For unfixable errors (rules without auto-fix), pair the PostToolUse hook with a Stop hook that runs uv run ruff check . after every turn. Add this to the same settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "uv run ruff check . >&2 || exit 2",
"timeout": 30
}
]
}
]
}
}The >&2 redirect sends Ruff’s diagnostics to stderr, and exit 2 marks the hook as a blocking failure that needs to be surfaced to the model. The next turn sees the unfixed errors and can address them. On a codebase with hundreds of pre-existing violations, scope the command to the path Claude is editing (uv run ruff check src/newmodule/) rather than the whole tree, or Claude wastes turns chasing legacy lint.
Run Ruff in pre-commit before commits
Hooks fire while Claude is editing; pre-commit fires when anyone (Claude included) tries to commit. The two are complementary. Pre-commit catches the case where Claude bypasses or misconfigures its own hooks, and it also gates contributions from teammates who aren’t using Claude at all.
Astral maintains an official ruff-pre-commit repo. Add this to .pre-commit-config.yaml at your project root, replacing the rev: with the latest release tag:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-formatInstall the hooks once with prek (the faster reimplementation of pre-commit) or pre-commit itself:
uvx prek install
# or
uvx pre-commit installNow git commit runs Ruff before recording the commit. If Ruff fixes anything, the commit aborts and you re-stage the changes; if it reports an unfixable error, you address it before the commit can complete.
When Claude commits a change (with git commit in the bash tool), the same hook runs. If pre-commit modifies files, Claude sees the abort message and can stage the fixes on the next turn.
For the full pre-commit setup including mypy, ty, and other Python tools, see How to set up pre-commit hooks for a Python project.
Combine all four layers
A project that uses every layer ends up with four small files. Here is the setup.
CLAUDE.md (project root):
## Linting and formatting
This project uses Ruff for both linting and formatting. Always invoke
through `uv run`. When working with Ruff, invoke `/astral:ruff` to follow
Astral's recommended usage.
- Lint: `uv run ruff check .`
- Lint and auto-fix: `uv run ruff check --fix .`
- Format: `uv run ruff format .`
- Configuration lives in `pyproject.toml` under `[tool.ruff]`..claude/settings.json (project root):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ruff-after-edit.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "uv run ruff check . >&2 || exit 2",
"timeout": 30
}
]
}
]
}
}.pre-commit-config.yaml (project root):
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-formatPlus the auto-format hook script at .claude/hooks/ruff-after-edit.sh from the section above. Commit all four files so the configuration travels with the repository and applies for every collaborator.
Important
The Astral plugin and the auto-format hook are not redundant. The plugin teaches Claude how to use Ruff when it intentionally invokes it; the hook formats every Python edit unconditionally. Keep both: one is documentation, the other is enforcement.
Learn More
- How to write Claude Code hooks for Python projects for hooks beyond Ruff (block pip, inject reminders, run a type checker)
- How to install the Astral plugins for Claude Code for the team-wide plugin install
- How to configure recommended Ruff defaults for a starter
[tool.ruff.lint]configuration - How to set up pre-commit hooks for a Python project for the broader pre-commit setup
- Ruff reference
- Claude Code hooks documentation
- Astral Claude Code plugins repository