Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ A simple modern Python project template powered by [Copier](https://copier.readt
- 🐳 **Docker Support**: Complete Docker development environment
- 📦 **Devcontainer Support**: VS Code devcontainer for consistent development
- ✨ **AI Editor Support**: [AGENTS.md](https://agents.md) and
[CLAUDE.md](https://docs.anthropic.com/en/docs/claude-code/overview) included for AI-powered development
[CLAUDE.md](https://docs.anthropic.com/en/docs/claude-code/overview) included for AI-powered development,
plus shared Claude Code / Codex hooks that format and lint Python files as the agent edits them
- 📝 **Type Hints**: Full type annotation support with modern Python features
- 🔎 **Type Checking**: Pre-configured [ty](https://docs.astral.sh/ty/) for static type checking
- 🔍 **Code Quality**: Pre-configured Ruff for linting and formatting
Expand Down Expand Up @@ -125,6 +126,10 @@ your-project/

- [AGENTS.md(`./template/AGENTS.md`)](https://agents.md)
- [CLAUDE.md(`./template/CLAUDE.md`)](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports)
- `.claude/settings.json`: Claude Code permissions (pre-approved `uv run --frozen`, `uv sync`, read-only git
commands; `.env` files denied) and a `PostToolUse` hook
- `.codex/hooks.json`: the same `PostToolUse` hook for Codex
- `.agents/hooks/format-python.sh`: the hook script, runs `ruff format` and `ruff check --fix` on the edited file

## Q&A

Expand Down
31 changes: 31 additions & 0 deletions template/.agents/hooks/format-python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# PostToolUse hook shared by Claude Code and Codex: format and lint the Python
# files the agent just edited. Reads the hook input JSON from stdin.
#
# Claude Code (Edit/Write) reports the file in tool_input.file_path.
# Codex (apply_patch) reports the whole patch in tool_input.command.
# Remaining lint errors are sent back to the agent via exit code 2.

set -euo pipefail

input="$(cat)"

files=()
while IFS= read -r file; do
[[ "${file}" == *.py ]] && files+=("${file}")
done < <(jq -r '
(.tool_input.file_path // empty),
((.tool_input.command // "") | scan("\\*\\*\\* (?:Add|Update) File: (.+)") | .[0])
' <<<"${input}")

# Avoid the array-length expansion here: its brace-hash prefix would start a Jinja
# comment when Copier renders this file.
[[ -n "${files[*]:-}" ]] || exit 0

cwd="$(jq -r '.cwd // empty' <<<"${input}")"
cd -- "${cwd:-$(git rev-parse --show-toplevel)}"

uv run --frozen ruff format -- "${files[@]}"
if ! uv run --frozen ruff check --fix -- "${files[@]}" >&2; then
exit 2
fi
29 changes: 29 additions & 0 deletions template/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"permissions": {
"allow": [
"Bash(uv run --frozen *)",
"Bash(uv sync *)",
"Bash(uv lock *)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)"
],
"deny": [
"Read(.env)",
"Read(*.env)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash \"$(git rev-parse --show-toplevel)/.agents/hooks/format-python.sh\""
}
]
}
]
}
}
17 changes: 17 additions & 0 deletions template/.codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "Format and lint Python files after the agent edits them.",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash \"$(git rev-parse --show-toplevel)/.agents/hooks/format-python.sh\"",
"statusMessage": "Formatting Python"
}
]
}
]
}
}
6 changes: 5 additions & 1 deletion template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,8 @@ tags
.history
.ionide

# End of https://www.toptal.com/developers/gitignore/api/linux,macos,visualstudiocode,vim,python,jupyternotebooks,venv,virtualenv
# End of https://www.toptal.com/developers/gitignore/api/linux,macos,visualstudiocode,vim,python,jupyternotebooks,venv,virtualenv

### AI coding agents ###
# Personal Claude Code overrides (machine-specific settings, never shared)
.claude/settings.local.json
16 changes: 13 additions & 3 deletions template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,28 @@ Follow these guidelines precisely.
- ONLY use uv, NEVER pip
- Installation: `uv add package`
- Upgrading: `uv add --dev package --upgrade-package package`
- FORBIDDEN: `uv pip install`, `@latest` syntax
- FORBIDDEN: `uv pip install`, `@latest` syntax, editing `uv.lock` by hand

2. Code Quality
- Type hints required for all code
- Imports used only in type annotations go under `if TYPE_CHECKING:` with
`from __future__ import annotations` at the top of the module (Ruff `TC` rules)
- Follow existing patterns exactly
- Use Google style for docstring

3. Testing Requirements
- Framework: `uv run --frozen pytest`
- Framework: `uv run --frozen pytest` (runs in parallel; use `-n 0` for `--pdb`)
- Coverage: test edge cases and errors
- Coverage report: `uv run --frozen pytest --cov`
- Coverage report: `uv run --frozen pytest --cov` (CI fails below 80% branch coverage of `src/`)
- New features require tests
- Bug fixes require regression tests

4. Git
- Follow the Conventional Commits style on commit messages.
- NEVER use `git commit --no-verify`; fix what the hooks report instead.

5. Running
- Application: `uv run {{project_name}}` (or `python -m {{package_name}}`)

## Code Formatting and Linting

Expand All @@ -39,3 +45,7 @@ Follow these guidelines precisely.
- Install: `uv run prek install`
- Runs: on git commit
- Tools: uv lock, Ruff, ty
4. Agent Hooks
- `.agents/hooks/format-python.sh` formats and lints every Python file right
after Claude Code or Codex edits it (wired in `.claude/settings.json` and
`.codex/hooks.json`). Do not re-run the formatter manually after edits.