Shared style enforcement and Claude Code tooling for xorq Python projects.
pip install xorq-styleOr as a dev dependency (with uv):
uv sync --group devAdd to your project's .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "{ command -v xorq-check-style >/dev/null 2>&1 || exit 0; } && xorq-check-style --hook"
}
]
}
]
}
}The hook runs after every Edit or Write tool call. It reads the tool input from stdin, checks the written file against xorq style rules, and exits non-zero if there are violations — causing Claude Code to see and fix them.
xorq-check-style src/myproject/foo.py src/myproject/bar.py
xorq-check-style --list # show all rules
xorq-check-style --disable=print,dataclasses . # skip specific rulesPipe any unified diff to --diff to lint only the lines that were added or modified:
git diff | xorq-check-style --diff # unstaged changes
git diff HEAD~3 | xorq-check-style --diff # last 3 commits
git diff main | xorq-check-style --diff # changes vs main branchThis gives the same changed-line scoping that the Claude Code hook gets automatically.
Add --json to any invocation to get machine-readable output on stdout:
xorq-check-style --json src/myproject/foo.py
git diff | xorq-check-style --diff --jsonEach violation is an object with filepath, line, rule, and message fields:
[
{"filepath": "src/foo.py", "line": 12, "rule": "os-path", "message": "import os.path (use pathlib.Path instead)"}
]JSON is written to stdout; in text mode (the default), violations go to stderr. Both modes exit 0 when clean and exit 2 when there are violations.
# one-time install to standard location
xorq-check-style install-completion bash # or zsh, fish
# or eval in your shell config
eval "$(xorq-check-style completion bash)"The --disable option supports tab completion for rule names, including comma-separated lists.
| Rule | Description |
|---|---|
relative-import |
No relative imports (use absolute imports) |
test-class |
No test classes (use plain test functions) |
deferred-import-test |
No deferred imports in test files |
deferred-stdlib |
No deferred stdlib imports (anywhere) |
os-environ |
No os.environ outside common/utils/ |
future-annotations |
Missing from __future__ import annotations |
os-path |
No os.path (use pathlib.Path) |
dataclasses |
No dataclasses (use attrs) |
cache-method |
No @functools.cache/lru_cache on methods (leaks memory via self) |
exception-hierarchy |
Custom exceptions must inherit from XorqError |
print |
No bare print() in library code (use logging/click.echo) |
type-annotations |
Functions must have type annotations |
unlisted-import |
Imported name not listed in target module's __all__ |
init-reexport |
Non-__init__ module re-exports imported name via __all__ |
Add a [tool.xorq-style] section to your pyproject.toml:
[tool.xorq-style]
disable = ["dataclasses", "os-path"] # disable rules globally
[tool.xorq-style.os-environ]
allow-paths = ["common/", "utils/"] # allow os.environ in these paths
[tool.xorq-style.exception-hierarchy]
base-class = "XorqError" # expected base class (default: XorqError)
[tool.xorq-style.print]
allow-files = ["cli.py"] # allow print() in these filenames
[tool.xorq-style.unlisted-import]
src-roots = ["python/"] # source roots relative to pyproject.toml (default: ["src", "."])All fields are optional. The config file is discovered by walking up from the checked file's directory.
Suppress a rule on a single line with a trailing comment:
from dataclasses import dataclass # xorq-style: disable=dataclassesMultiple rules can be comma-separated:
import os.path # xorq-style: disable=os-path,deferred-stdlib