# How to configure Ruff with Claude Code


[Claude Code](https://code.claude.com/) does not run [Ruff](https://pydevtools.com/handbook/reference/ruff.md) 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
- OpenAI's Astral plugin's `/astral:ruff` skill for on-demand Ruff guidance
- A PostToolUse hook that auto-formats every Python edit
- A [pre-commit](https://pre-commit.com/) gate that catches anything Claude missed before commit time

> [!TIP]
> Starting a new project? The [project setup tutorial](https://pydevtools.com/handbook/tutorial/set-up-a-python-project-for-claude-code.md) builds all four layers on a fresh uv package from scratch.

## Prerequisites

* [Claude Code](https://code.claude.com/) installed
* [uv](https://pydevtools.com/handbook/reference/uv.md) installed
* A Python project with Ruff added as a dev dependency (`uv add --dev ruff`)
* `jq` installed 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](#tell-claude-code-to-use-ruff-with-claudemd) |
| Give Claude up-to-date Ruff usage guidance on demand (rule selection, migrating from [Black](https://pydevtools.com/handbook/reference/black.md) or [flake8](https://pydevtools.com/handbook/reference/flake8.md)) | [`/astral:ruff` skill](#add-the-astral-plugins-ruff-skill) |
| Auto-format every Python file Claude writes or edits | [PostToolUse hook](#auto-format-every-python-edit-with-a-hook) |
| Catch unfixable lint errors before they reach the repo, even when Claude is not the one committing | [pre-commit](#run-ruff-in-pre-commit-before-commits) |

## 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`:

```markdown {filename="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](https://code.claude.com/docs/en/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](https://pydevtools.com/handbook/reference/pytest.md), and pre-commit guidance, use the [CLAUDE.md template for Python projects](https://pydevtools.com/handbook/how-to/how-to-use-the-pydevtools-claude-md-template.md) instead of writing one from scratch.

For projects that don't yet have a curated Ruff configuration, see [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md); Claude will follow whatever rules `pyproject.toml` declares.

## Add OpenAI's Astral plugin's Ruff skill

OpenAI publishes an [official Claude Code plugin](https://github.com/astral-sh/claude-code-plugins) 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-sh
```

Once installed, ask Claude to "format this module with `/astral:ruff`" or "migrate this Black config to Ruff using `/astral:ruff`" and it follows OpenAI'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`:

```markdown {filename="CLAUDE.md"}
When working with Ruff, invoke `/astral:ruff` to follow OpenAI'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 OpenAI's Astral plugins for Claude Code](https://pydevtools.com/handbook/how-to/how-to-install-astral-plugins-for-claude-code.md).

## 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:

```bash {filename=".claude/hooks/ruff-after-edit.sh"}
#!/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 0
```

Make it executable:

```bash
chmod +x .claude/hooks/ruff-after-edit.sh
```

Register 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):

```json {filename=".claude/settings.json"}
{
  "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:

```bash
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 output
```

The 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](https://pydevtools.com/handbook/how-to/how-to-write-claude-code-hooks-for-python-projects.md) 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`:

```json {filename=".claude/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.

OpenAI maintains an [official `ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit) repo. Add this to `.pre-commit-config.yaml` at your project root, replacing the `rev:` with the [latest release tag](https://github.com/astral-sh/ruff-pre-commit/releases):

```yaml {filename=".pre-commit-config.yaml"}
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.14
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format
```

Install the hooks once with [prek](https://pydevtools.com/handbook/reference/prek.md) (the faster reimplementation of pre-commit) or pre-commit itself:

```bash
uvx prek install
# or
uvx pre-commit install
```

Now `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](https://pydevtools.com/handbook/reference/mypy.md), [ty](https://pydevtools.com/handbook/reference/ty.md), and other Python tools, see [How to set up pre-commit hooks for a Python project](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md).

## Combine all four layers

A project that uses every layer ends up with four small files. Here is the setup.

`CLAUDE.md` (project root):

```markdown {filename="CLAUDE.md"}
## 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
OpenAI'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):

```json {filename=".claude/settings.json"}
{
  "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):

```yaml {filename=".pre-commit-config.yaml"}
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.14
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format
```

Plus 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]
> OpenAI's 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](https://pydevtools.com/handbook/how-to/how-to-write-claude-code-hooks-for-python-projects.md) for hooks beyond Ruff (block pip, inject reminders, run a type checker)
* [How to install OpenAI's Astral plugins for Claude Code](https://pydevtools.com/handbook/how-to/how-to-install-astral-plugins-for-claude-code.md) for the team-wide plugin install
* [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md) for a starter `[tool.ruff.lint]` configuration
* [How to set up pre-commit hooks for a Python project](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md) for the broader pre-commit setup
* [Ruff reference](https://pydevtools.com/handbook/reference/ruff.md)
* [Claude Code hooks documentation](https://code.claude.com/docs/en/hooks)
* [OpenAI's Astral Claude Code plugins repository](https://github.com/astral-sh/claude-code-plugins)
