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
108 changes: 108 additions & 0 deletions .claude/rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# AI Rules — Developer Guide

This directory contains the rules that inform any AI assistant (Claude Code, Cursor, etc.)
working on this codebase. Rules are plain Markdown files — no tool-specific frontmatter or
format is required. Any AI that can read context from a directory will benefit from them.

## Structure

Rules are organised into a **common** layer plus **language/tool-specific** directories:

```
.claude/rules/
├── README.md ← you are here
├── common/ # Universal principles — apply to all code in this repo
│ ├── coding-style.md
│ ├── git-workflow.md
│ ├── testing.md
│ ├── security.md
│ ├── development-workflow.md
│ └── code-review.md
├── python/ # Python-specific (extends common/)
│ ├── coding-style.md
│ ├── testing.md
│ ├── patterns.md
│ ├── security.md
│ └── hooks.md
├── jinja/ # Jinja2 template-specific
│ ├── coding-style.md
│ └── testing.md
├── bash/ # Shell script-specific
│ ├── coding-style.md
│ └── security.md
├── markdown/ # Markdown authoring conventions
│ └── conventions.md
├── yaml/ # YAML file conventions (copier.yml, workflows, etc.)
│ └── conventions.md
└── copier/ # Copier template-specific rules (this repo only)
└── template-conventions.md
```

## Rule priority

When language-specific rules and common rules conflict, **language-specific rules take
precedence** (specific overrides general). This mirrors CSS specificity and `.gitignore`
precedence.

- `common/` defines universal defaults.
- Language directories (`python/`, `jinja/`, `bash/`, …) override those defaults where
language idioms differ.

## Dual-hierarchy reminder

This Copier meta-repo has **two parallel rule trees**:

```
.claude/rules/ ← active when DEVELOPING this template repo
template/.claude/rules/ ← rendered into every GENERATED project
```

When you add or modify a rule:
- Changes to `template/.claude/rules/` affect every project generated from this
template going forward.
- Changes to the root `.claude/rules/` affect only this meta-repo.
- Many rules belong in **both** trees (e.g. Python coding style, security).
- Copier-specific rules (`copier/`) belong only in the root tree.
- Jinja rules belong only in the root tree (generated projects do not contain Jinja files).

## How to write a new rule

1. **Choose the right directory** — `common/` for language-agnostic principles,
a language directory for language-specific ones. Create a new directory if a
language or domain does not exist yet.

2. **File name** — use lowercase kebab-case matching the topic: `coding-style.md`,
`testing.md`, `patterns.md`, `security.md`, `hooks.md`, `performance.md`.

3. **Opening line** — if the file extends a common counterpart, start with:
```
> This file extends [common/xxx.md](../common/xxx.md) with <Language> specific content.
```

4. **Content guidelines**:
- State rules as actionable imperatives ("Always …", "Never …", "Prefer …").
- Use concrete code examples (correct and incorrect) wherever possible.
- Keep each file under 150 lines; split into multiple files if a topic grows larger.
- Do not repeat content already covered in the common layer — cross-reference instead.
- Avoid tool-specific configuration syntax in rule prose; describe intent, not config.

5. **File patterns annotation** (optional but helpful) — add a YAML comment block at the
top listing which glob patterns the rule applies to. AI tools that understand frontmatter
can use this; tools that do not will simply skip the comment:
```yaml
# applies-to: **/*.py, **/*.pyi
```

6. **Mirror to `template/.claude/rules/`** if the rule is relevant to generated projects.

7. **Update this README** when adding a new language directory or a new top-level file.

## How AI tools consume these rules

| Tool | Mechanism |
|------|-----------|
| Claude Code | Reads `CLAUDE.md` (project root), then any file you reference or load via slash commands |
| Cursor | Reads `.cursor/rules/*.mdc`; symlink or copy relevant rules there if desired |
| Generic LLM | Pass rule file contents in system prompt or context window |

Because rules are plain Markdown, they are readable by any tool without conversion.
135 changes: 135 additions & 0 deletions .claude/rules/bash/coding-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Bash / Shell Coding Style

# applies-to: **/*.sh

Shell scripts in this repository are hook scripts under `.claude/hooks/` and helper
scripts under `scripts/`. These rules apply to all `.sh` files.

## Shebang and strict mode

Every script must start with the appropriate shebang and enable strict mode:

```bash
#!/usr/bin/env bash
# One-line description of what this script does.
```

For PostToolUse / Stop / SessionStart hooks (non-blocking):

```bash
set -euo pipefail
```

For PreToolUse blocking hooks (must not exit non-zero accidentally):

```bash
set -uo pipefail # intentionally NOT -e; we handle exit codes manually
```

`-e` (exit on error), `-u` (error on unset variable), `-o pipefail` (pipe failures propagate).
Document clearly when `-e` is omitted and why.

## Variable quoting

Always quote variable expansions unless you intentionally want word splitting:

```bash
# Correct
file_path="$1"
if [[ -f "$file_path" ]]; then ...

# Wrong — breaks on paths with spaces
if [[ -f $file_path ]]; then ...
```

Use `[[ ]]` (bash conditionals) instead of `[ ]` (POSIX test). `[[ ]]` handles
spaces in variables without quoting issues.

## Reading stdin (hook scripts)

Hook scripts receive JSON on stdin. Always capture it immediately and parse with Python:

```bash
INPUT=$(cat)

FILE_PATH=$(python3 - <<'PYEOF'
import json, sys
data = json.loads(sys.stdin.read())
print(data.get("tool_input", {}).get("file_path", ""))
PYEOF
<<<"$INPUT") || { echo "$INPUT"; exit 0; }
```

The `|| { echo "$INPUT"; exit 0; }` guard ensures that a malformed JSON payload never
accidentally blocks a PreToolUse hook.

## Output formatting

Use box-drawing characters for structured output (consistent with all other hooks):

```bash
echo "┌─ Hook name: $context"
echo "│"
echo "│ Informational content"
echo "└─ ✓ Done" # or └─ ✗ Fix before committing
```

- PostToolUse / Stop / SessionStart hooks: print to **stdout**.
- PreToolUse blocking messages: print to **stderr** (shown to the user on block).

## Exit codes

| Script type | Exit 0 | Exit 2 |
|-------------|--------|--------|
| PreToolUse | Allow tool to proceed | Block tool call |
| PostToolUse | Normal completion | Not meaningful — avoid |
| Stop / SessionStart | Normal completion | Not meaningful — avoid |

Only PreToolUse hooks should ever exit 2. All other hooks must exit 0.

When a PreToolUse hook allows the call to proceed, echo `$INPUT` back to stdout (required):

```bash
echo "$INPUT" # pass-through: required for the tool call to proceed
exit 0
```

## Naming and file organisation

File naming convention:
```
{event-prefix}-{matcher}-{purpose}.sh
```

| Prefix | Lifecycle event |
|--------|----------------|
| `pre-bash-` | PreToolUse on Bash |
| `pre-write-` | PreToolUse on Write |
| `pre-config-` | PreToolUse on Edit/Write/MultiEdit |
| `pre-protect-` | PreToolUse guard for a specific resource |
| `post-edit-` | PostToolUse on Edit or Write |
| `post-bash-` | PostToolUse on Bash |
| `session-` | SessionStart |
| `stop-` | Stop |

## Functions

Extract repeated logic into shell functions. Name functions in `snake_case`:

```bash
check_python_file() {
local file_path="$1"
[[ "$file_path" == *.py ]] && [[ -f "$file_path" ]]
}
```

Keep functions short (≤ 30 lines). Scripts that grow beyond ~100 lines should be
split into multiple files or rewritten as Python.

## Portability

Hook scripts run on macOS and Linux. Avoid GNU-specific flags when a POSIX-compatible
alternative exists. Test both platforms if in doubt.

Copier `_tasks` use `/bin/sh` (POSIX shell, not bash). Use `#!/bin/sh` and POSIX-only
syntax in tasks embedded in `copier.yml`.
92 changes: 92 additions & 0 deletions .claude/rules/bash/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Bash Security

# applies-to: **/*.sh

> This file extends [common/security.md](../common/security.md) with Bash-specific content.

## Never use `eval`

`eval` executes arbitrary strings as code. Any user-controlled input passed to `eval`
is a code injection vulnerability:

```bash
# Wrong — if $user_input = "rm -rf /", this destroys the filesystem
eval "process_$user_input"

# Correct — use a case statement or associative array
case "$action" in
start) start_service ;;
stop) stop_service ;;
*) echo "Unknown action: $action" >&2; exit 1 ;;
esac
```

## Never use `shell=True` equivalent

Constructing commands via string interpolation and passing to a shell interpreter
enables injection:

```bash
# Wrong — $filename could contain shell metacharacters
system("process $filename")

# Correct — pass as separate argument
process_file "$filename"
```

When calling external programs, pass arguments as separate words, never concatenated
into a single string.

## Validate and sanitise all inputs

Scripts that accept arguments or read from environment variables must validate them
before use:

```bash
file_path="${1:?Usage: script.sh <file-path>}" # fail with message if empty

# Reject paths containing traversal sequences
if [[ "$file_path" == *..* ]]; then
echo "Error: path traversal not allowed" >&2
exit 1
fi
```

## Secrets in environment variables

- Do not echo or log environment variables that may contain secrets.
- Do not write secrets to temporary files unless the file is created with `mktemp`
and cleaned up in an `EXIT` trap.
- Check that required environment variables exist before using them:

```bash
: "${API_KEY:?API_KEY environment variable is required}"
```

## Temporary file handling

Use `mktemp` for temporary files and clean up with a trap:

```bash
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT

# Use $TMPFILE safely
some_command > "$TMPFILE"
process_output "$TMPFILE"
```

Never use predictable filenames like `/tmp/output.txt` — they are vulnerable to
symlink attacks.

## Subprocess calls in hook scripts

Hook scripts in `.claude/hooks/` execute in the context of the developer's machine.
They should:
- Only call trusted binaries (`uv`, `git`, `python3`, `ruff`, `basedpyright`).
- Never download or execute code from the network.
- Avoid `curl | bash` patterns.
- Not modify files outside the project directory.

The `pre-bash-block-no-verify.sh` hook blocks `git commit --no-verify` to ensure
pre-commit security gates cannot be bypassed.
Loading
Loading