From b2a6ed0e9f83ffadc05e871b91a1021482d77ce0 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Wed, 15 Apr 2026 17:21:29 +0200 Subject: [PATCH 1/8] Add AI coding agent instruction files - AGENTS.md as single source of truth for AI assistant context - CLAUDE.md as thin @AGENTS.md import for Claude Code - .claude/settings.json with shared permissions and hook wiring - .claude/hooks/session-start.sh to install deps on session start - .claude/hooks/post-edit-lint.sh for fast per-file linting - .claude/hooks/post-stop-validate.sh for full lint, test, and build --- .claude/hooks/post-edit-lint.sh | 9 +++++ .claude/hooks/post-stop-validate.sh | 6 +++ .claude/hooks/session-start.sh | 4 ++ .claude/settings.json | 63 +++++++++++++++++++++++++++++ AGENTS.md | 55 +++++++++++++++++++++++++ CLAUDE.md | 1 + 6 files changed, 138 insertions(+) create mode 100755 .claude/hooks/post-edit-lint.sh create mode 100755 .claude/hooks/post-stop-validate.sh create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh new file mode 100755 index 0000000..04cef87 --- /dev/null +++ b/.claude/hooks/post-edit-lint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +INPUT=$(cat) +FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +if [[ "$FILE" == *.py ]]; then + flake8 "$FILE" 2>&1 +fi + +exit 0 diff --git a/.claude/hooks/post-stop-validate.sh b/.claude/hooks/post-stop-validate.sh new file mode 100755 index 0000000..ffb980c --- /dev/null +++ b/.claude/hooks/post-stop-validate.sh @@ -0,0 +1,6 @@ +#!/bin/bash +flake8 ./chartmogul 2>&1 +python -m unittest 2>&1 +python setup.py sdist 2>&1 + +exit 0 diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..cbd8643 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pip install -e .[testing] 2>&1 + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..deecec9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,63 @@ +{ + "permissions": { + "allow": [ + "Bash(pip install:*)", + "Bash(python -m unittest:*)", + "Bash(python -m pytest:*)", + "Bash(flake8:*)", + "Bash(coverage:*)", + "Bash(python --version)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git branch:*)", + "Bash(git fetch:*)", + "Bash(git stash:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(git pull:*)", + "Bash(git cherry-pick:*)", + "Bash(git reset:*)", + "Bash(gh pr:*)", + "Bash(gh api:*)" + ], + "deny": [ + "Read(./.env*)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "./.claude/hooks/session-start.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "./.claude/hooks/post-edit-lint.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "./.claude/hooks/post-stop-validate.sh" + } + ] + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ce8776c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,55 @@ +# ChartMogul Python SDK + +Python SDK wrapping the ChartMogul API. Requires Python >= 3.10. Uses `requests` for HTTP with automatic retry and exponential backoff. + +## Commands + +- `pip install -e .[testing]` - install with test dependencies +- `python -m unittest` - run full test suite +- `python -m unittest test.api.test_customer` - run single test module +- `flake8 ./chartmogul` - lint source code +- `coverage run -m unittest && coverage xml -i --include='chartmogul/*'` - run tests with coverage + +Version is in `chartmogul/version.py`. + +## Architecture + +The class hierarchy is: `DataObject` (base with dynamic attributes and `__repr__`) -> `Resource` (adds HTTP via `requests`, Promise-based responses, URI template expansion, retry logic) -> Concrete resources (Customer, Invoice, Plan, etc.). + +Resources use Marshmallow for serialization. Each resource defines an inner `_Schema(Schema)` class with `@post_load` returning a class instance. Fields use `data_key` to map between Python snake_case and API kebab-case (e.g. `data_key="customer-since"`). + +Class-level attributes configure behavior: +- `_path`: URI template with `{/uuid}` syntax for optional path params +- `_root_key`: key name for list results (e.g. `"entries"`) +- `_many`: namedtuple for paginated list responses (entries + cursor + has_more) +- `_schema`: instantiated Schema with `unknown=EXCLUDE` +- `_bool_query_params`: list of query params to convert from Python bool to string + +HTTP methods are dynamically attached after class definition: +```python +Customer.all = Resource._method("all", "get") +Customer.create = Resource._method("create", "post") +``` + +Verb mapping: create->POST, all->GET, retrieve->GET, update->PUT, modify->PATCH, destroy->DELETE. + +All API calls return `Promise` objects. Use `.get()` for synchronous access. + +Nested objects that don't have their own API endpoint extend `DataObject` instead of `Resource`. + +## Testing + +Stack: unittest + requests_mock. Tests live in `test/api/test_.py`. CI runs on GitHub Actions with Python 3.10-3.14. + +Test pattern: +1. Define fixture dicts at module level matching API JSON responses +2. Use `@requests_mock.mock()` decorator +3. Register mock URI with expected headers, status, and JSON response +4. Call the SDK method with `Config("token")` and `.get()` for sync result +5. Assert call count, request body/params, and deserialized response types + +When adding a new resource, create a matching `test/api/test_.py` following this pattern. + +## Code style + +snake_case methods, PascalCase classes, UPPER_SNAKE_CASE constants. Leading underscore for internal attributes (`_path`, `_schema`). Linting via flake8 with max line length 100. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From aadd82dfa22c443bd5ca081fee1fae81cb5af6ca Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Wed, 15 Apr 2026 17:21:34 +0200 Subject: [PATCH 2/8] Add .gitignore rules for AI tool ephemeral data Ignore plans, todos, local settings, and worktrees from Claude Code and data directories from Cursor, Aider, Codeium, Copilot, Windsurf, Cline, and Codex. Shared config (.claude/settings.json, hooks) remains tracked. --- .gitignore | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3b065ea..e12824a 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,17 @@ ENV/ # Rope project settings .ropeproject -/.idea \ No newline at end of file +/.idea + +# AI tools - ephemeral data (shared config is tracked) +.claude/plans/ +.claude/todos/ +.claude/settings.local.json +.claude/worktrees/ +.cursor/ +.aider* +.codeium/ +.copilot/ +.windsurf/ +.cline/ +.codex/ \ No newline at end of file From f3587d1e33008b827dba5935adab3f1c23564fa7 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 09:09:39 +0200 Subject: [PATCH 3/8] Improve Claude Code hook architecture - SessionStart: detect repo state (branch, dirty files, missing deps) instead of running pip install. No network, <1s, idempotent. - PostToolUse: record edited file path only (<50ms). No linting per edit - batched to Stop instead. - Stop: batch-lint touched files with flake8, run test suite (~0.13s). Report lint issues and test failures as additionalContext so the next turn sees them. Silent on clean runs. - Add MultiEdit to PostToolUse matcher in settings.json. --- .claude/hooks/post-edit-lint.sh | 17 ++++++++++-- .claude/hooks/post-stop-validate.sh | 43 +++++++++++++++++++++++++++-- .claude/hooks/session-start.sh | 31 ++++++++++++++++++++- .claude/settings.json | 3 +- 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh index 04cef87..68dbc18 100755 --- a/.claude/hooks/post-edit-lint.sh +++ b/.claude/hooks/post-edit-lint.sh @@ -1,9 +1,20 @@ #!/bin/bash +# PostToolUse (Edit|Write): record touched file for batch processing in Stop. +# Must be <50ms. No linting, no formatting, no git calls. +cd "$CLAUDE_PROJECT_DIR" || exit 0 + INPUT=$(cat) FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -if [[ "$FILE" == *.py ]]; then - flake8 "$FILE" 2>&1 -fi +[[ -z "$FILE" ]] && exit 0 +[[ "$FILE" != *.py ]] && exit 0 + +# Skip generated/vendored paths +case "$FILE" in + *__pycache__*|*.egg-info*|*/dist/*|*/build/*|*/node_modules/*|*/vendor/*|*/coverage/*) exit 0 ;; +esac + +# Append to tracker (dedup at read time) +echo "$FILE" >> /tmp/claude-py-touched-files 2>/dev/null || true exit 0 diff --git a/.claude/hooks/post-stop-validate.sh b/.claude/hooks/post-stop-validate.sh index ffb980c..8c7d63b 100755 --- a/.claude/hooks/post-stop-validate.sh +++ b/.claude/hooks/post-stop-validate.sh @@ -1,6 +1,43 @@ #!/bin/bash -flake8 ./chartmogul 2>&1 -python -m unittest 2>&1 -python setup.py sdist 2>&1 +# Stop: batch-lint files touched this turn, run test suite. +# Python has no autoformatter configured, so lint + tests only. +# Test suite runs in ~0.13s so it fits within the Stop budget. +cd "$CLAUDE_PROJECT_DIR" || exit 0 + +TRACKER="/tmp/claude-py-touched-files" +[[ ! -f "$TRACKER" ]] && exit 0 + +# Dedup file list, filter to files that still exist +files=() +while IFS= read -r f; do + [[ -f "$f" ]] && files+=("$f") +done < <(sort -u "$TRACKER") + +# Clear tracker for next turn +: > "$TRACKER" 2>/dev/null || true + +[[ ${#files[@]} -eq 0 ]] && exit 0 + +# Lint only the touched files +output=$(python3 -m flake8 "${files[@]}" 2>&1) || true + +# Run test suite (~0.13s) +test_output=$(python3 -m unittest 2>&1) +test_rc=$? +if [[ $test_rc -ne 0 ]]; then + test_failures=$(echo "$test_output" | tail -20) + output+=$'\n'"TEST FAILURES:\n$test_failures" +fi + +if [[ -n "$output" ]]; then + # Truncate to keep context short + truncated=$(echo "$output" | head -20) + total=$(echo "$output" | wc -l | tr -d ' ') + if [[ "$total" -gt 20 ]]; then + truncated+=$'\n'"... ($total total lint issues, showing first 20)" + fi + jq -n --arg ctx "$truncated" \ + '{"hookSpecificOutput": {"hookEventName": "Stop", "additionalContext": $ctx}}' +fi exit 0 diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index cbd8643..d95b2a4 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,4 +1,33 @@ #!/bin/bash -pip install -e .[testing] 2>&1 +# SessionStart: detect repo state, warn about stale deps. Must be <1s, no network. +cd "$CLAUDE_PROJECT_DIR" || exit 0 + +context="" + +# Check for uncommitted changes +dirty=$(git status --porcelain 2>/dev/null | head -5) +if [[ -n "$dirty" ]]; then + count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + context+="Uncommitted changes ($count files). " +fi + +# Check current branch +branch=$(git branch --show-current 2>/dev/null) +if [[ -n "$branch" ]]; then + context+="On branch: $branch. " +fi + +# Check if deps are installed +if ! python -c "import chartmogul" 2>/dev/null; then + context+="WARNING: chartmogul package not installed. Run: pip install -e .[testing]" +fi + +# Reset file tracker for this session +: > /tmp/claude-py-touched-files 2>/dev/null || true + +if [[ -n "$context" ]]; then + jq -n --arg ctx "$context" \ + '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": $ctx}}' +fi exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index deecec9..7f8125a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,6 +6,7 @@ "Bash(python -m pytest:*)", "Bash(flake8:*)", "Bash(coverage:*)", + "Bash(python setup.py sdist:*)", "Bash(python --version)", "Bash(git status:*)", "Bash(git log:*)", @@ -40,7 +41,7 @@ ], "PostToolUse": [ { - "matcher": "Edit|Write", + "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", From 539cb3f36fb8a1706b2a348a7ce9aee842dc922b Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 09:37:42 +0200 Subject: [PATCH 4/8] Align hook architecture with Ruby SDK patterns - Add session-scoped file tracker via CLAUDE_ENV_FILE to prevent cross-session collisions (matches Ruby SDK approach) - Rename session-start.sh to session-start-setup.sh for consistency - Fix python -> python3 usage throughout all hooks - Fix double git status call in session start - Run tests unconditionally in Stop (0.13s cost, catches regressions even when only Bash commands were used) - Extract structured lint offenses and test failure summaries instead of raw output - Align .gitignore AI tool entries with Ruby SDK (add .claude/memory, scheduled-tasks, credentials.json, CLAUDE.local.md, .continue, .junie, granular .cursor paths) --- .claude/hooks/post-edit-lint.sh | 18 ++++----- .claude/hooks/post-stop-validate.sh | 59 ++++++++++++++-------------- .claude/hooks/session-start-setup.sh | 40 +++++++++++++++++++ .claude/hooks/session-start.sh | 33 ---------------- .claude/settings.json | 2 +- .gitignore | 22 +++++++---- 6 files changed, 92 insertions(+), 82 deletions(-) create mode 100755 .claude/hooks/session-start-setup.sh delete mode 100755 .claude/hooks/session-start.sh diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh index 68dbc18..8beec39 100755 --- a/.claude/hooks/post-edit-lint.sh +++ b/.claude/hooks/post-edit-lint.sh @@ -1,20 +1,16 @@ #!/bin/bash -# PostToolUse (Edit|Write): record touched file for batch processing in Stop. +# PostToolUse (Edit|Write|MultiEdit): record touched file for batch processing in Stop. # Must be <50ms. No linting, no formatting, no git calls. cd "$CLAUDE_PROJECT_DIR" || exit 0 INPUT=$(cat) FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -[[ -z "$FILE" ]] && exit 0 -[[ "$FILE" != *.py ]] && exit 0 - -# Skip generated/vendored paths -case "$FILE" in - *__pycache__*|*.egg-info*|*/dist/*|*/build/*|*/node_modules/*|*/vendor/*|*/coverage/*) exit 0 ;; -esac - -# Append to tracker (dedup at read time) -echo "$FILE" >> /tmp/claude-py-touched-files 2>/dev/null || true +# Only track .py files, skip vendored/generated paths +if [[ "$FILE" == *.py ]] && [[ "$FILE" != *__pycache__* ]] && [[ "$FILE" != *.egg-info* ]] && [[ "$FILE" != */dist/* ]] && [[ "$FILE" != */build/* ]]; then + TRACKER="/tmp/claude-edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" + echo "$FILE" >> "$TRACKER" + sort -u "$TRACKER" -o "$TRACKER" +fi exit 0 diff --git a/.claude/hooks/post-stop-validate.sh b/.claude/hooks/post-stop-validate.sh index 8c7d63b..45384c3 100755 --- a/.claude/hooks/post-stop-validate.sh +++ b/.claude/hooks/post-stop-validate.sh @@ -1,43 +1,42 @@ #!/bin/bash -# Stop: batch-lint files touched this turn, run test suite. +# Stop: batch-lint touched files, run test suite (~0.13s). # Python has no autoformatter configured, so lint + tests only. -# Test suite runs in ~0.13s so it fits within the Stop budget. cd "$CLAUDE_PROJECT_DIR" || exit 0 -TRACKER="/tmp/claude-py-touched-files" -[[ ! -f "$TRACKER" ]] && exit 0 +TRACKER="/tmp/claude-edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" -# Dedup file list, filter to files that still exist -files=() -while IFS= read -r f; do - [[ -f "$f" ]] && files+=("$f") -done < <(sort -u "$TRACKER") +ctx="" -# Clear tracker for next turn -: > "$TRACKER" 2>/dev/null || true +# Batch flake8 on tracked files (if any were edited) +if [[ -f "$TRACKER" ]]; then + files=$(cat "$TRACKER") + rm -f "$TRACKER" -[[ ${#files[@]} -eq 0 ]] && exit 0 - -# Lint only the touched files -output=$(python3 -m flake8 "${files[@]}" 2>&1) || true + if [[ -n "$files" ]]; then + lint_out=$(echo "$files" | xargs python3 -m flake8 2>&1) || true + offenses=$(echo "$lint_out" | grep -E "^.+:[0-9]+:[0-9]+:" | head -20) + if [[ -n "$offenses" ]]; then + ctx+="flake8 offenses:\n$offenses\n" + fi + fi +fi -# Run test suite (~0.13s) -test_output=$(python3 -m unittest 2>&1) -test_rc=$? -if [[ $test_rc -ne 0 ]]; then - test_failures=$(echo "$test_output" | tail -20) - output+=$'\n'"TEST FAILURES:\n$test_failures" +# Always run tests - edits to tests or source both matter (~0.13s) +test_out=$(python3 -m unittest 2>&1) +test_exit=$? +if [[ $test_exit -ne 0 ]]; then + summary=$(echo "$test_out" | grep -E "^(FAILED|ERROR)" | tail -1) + failures=$(echo "$test_out" | grep -E "^(FAIL|ERROR):" | head -10) + ctx+="tests failed ($summary):\n$failures\n" fi -if [[ -n "$output" ]]; then - # Truncate to keep context short - truncated=$(echo "$output" | head -20) - total=$(echo "$output" | wc -l | tr -d ' ') - if [[ "$total" -gt 20 ]]; then - truncated+=$'\n'"... ($total total lint issues, showing first 20)" - fi - jq -n --arg ctx "$truncated" \ - '{"hookSpecificOutput": {"hookEventName": "Stop", "additionalContext": $ctx}}' +if [[ -n "$ctx" ]]; then + jq -n --arg ctx "$ctx" '{ + "hookSpecificOutput": { + "hookEventName": "Stop", + "additionalContext": $ctx + } + }' fi exit 0 diff --git a/.claude/hooks/session-start-setup.sh b/.claude/hooks/session-start-setup.sh new file mode 100755 index 0000000..387022e --- /dev/null +++ b/.claude/hooks/session-start-setup.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# SessionStart: generate session ID, detect repo state. Must be <1s, no network. +cd "$CLAUDE_PROJECT_DIR" || exit 0 + +# Generate a stable session ID and persist via CLAUDE_ENV_FILE +# so the edit tracker and stop hook share the same file path +SESSION_ID="$(date +%s)-$$" +if [[ -n "$CLAUDE_ENV_FILE" ]]; then + echo "export CLAUDE_HOOK_SESSION_ID='$SESSION_ID'" >> "$CLAUDE_ENV_FILE" +fi + +ctx="" + +# Warn about uncommitted changes +dirty=$(git diff --name-only 2>/dev/null | head -5) +if [[ -n "$dirty" ]]; then + ctx+="Uncommitted changes:\n$dirty\n" +fi + +# Report current branch +branch=$(git branch --show-current 2>/dev/null) +if [[ -n "$branch" ]]; then + ctx+="Branch: $branch\n" +fi + +# Check if deps are installed +if ! python3 -c "import chartmogul" 2>/dev/null; then + ctx+="WARNING: chartmogul package not installed. Run: pip install -e .[testing]\n" +fi + +if [[ -n "$ctx" ]]; then + jq -n --arg ctx "$ctx" '{ + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": $ctx + } + }' +fi + +exit 0 diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh deleted file mode 100755 index d95b2a4..0000000 --- a/.claude/hooks/session-start.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# SessionStart: detect repo state, warn about stale deps. Must be <1s, no network. -cd "$CLAUDE_PROJECT_DIR" || exit 0 - -context="" - -# Check for uncommitted changes -dirty=$(git status --porcelain 2>/dev/null | head -5) -if [[ -n "$dirty" ]]; then - count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') - context+="Uncommitted changes ($count files). " -fi - -# Check current branch -branch=$(git branch --show-current 2>/dev/null) -if [[ -n "$branch" ]]; then - context+="On branch: $branch. " -fi - -# Check if deps are installed -if ! python -c "import chartmogul" 2>/dev/null; then - context+="WARNING: chartmogul package not installed. Run: pip install -e .[testing]" -fi - -# Reset file tracker for this session -: > /tmp/claude-py-touched-files 2>/dev/null || true - -if [[ -n "$context" ]]; then - jq -n --arg ctx "$context" \ - '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": $ctx}}' -fi - -exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 7f8125a..3f5d797 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -34,7 +34,7 @@ "hooks": [ { "type": "command", - "command": "./.claude/hooks/session-start.sh" + "command": "./.claude/hooks/session-start-setup.sh" } ] } diff --git a/.gitignore b/.gitignore index e12824a..4fedfc7 100644 --- a/.gitignore +++ b/.gitignore @@ -92,15 +92,23 @@ ENV/ /.idea -# AI tools - ephemeral data (shared config is tracked) +# AI agent local/ephemeral data .claude/plans/ -.claude/todos/ -.claude/settings.local.json +.claude/memory/ .claude/worktrees/ -.cursor/ +.claude/scheduled-tasks/ +.claude/settings.local.json +.claude/todos/ +.claude/credentials.json +CLAUDE.local.md +.cursor/chat/ +.cursor/composer/ +.cursor/mcp.json +.windsurf/ +.cline/ +.continue/ +.junie/ .aider* +.codex/ .codeium/ .copilot/ -.windsurf/ -.cline/ -.codex/ \ No newline at end of file From 478a446cb76e70297c5be4455f21c12453ba178c Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 14:35:17 +0200 Subject: [PATCH 5/8] Address PR review feedback - .gitignore: use .claude/* with negation for hooks/ and settings.json instead of listing individual subdirs. Use .cursor/ instead of individual subdirs. (loomchild) - AGENTS.md: remove Python version and CI mention from testing section - tools can read version from setup.py/CI config (loomchild) - settings.json: remove gh api from default allow list (pkopac) --- .claude/settings.json | 3 +-- .gitignore | 18 ++++++------------ AGENTS.md | 2 +- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 3f5d797..9fb0155 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,8 +21,7 @@ "Bash(git pull:*)", "Bash(git cherry-pick:*)", "Bash(git reset:*)", - "Bash(gh pr:*)", - "Bash(gh api:*)" + "Bash(gh pr:*)" ], "deny": [ "Read(./.env*)" diff --git a/.gitignore b/.gitignore index 4fedfc7..cb2a0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -92,18 +92,12 @@ ENV/ /.idea -# AI agent local/ephemeral data -.claude/plans/ -.claude/memory/ -.claude/worktrees/ -.claude/scheduled-tasks/ -.claude/settings.local.json -.claude/todos/ -.claude/credentials.json +# AI agent local/ephemeral data (shared config is tracked via negation) +.claude/* +!.claude/hooks/ +!.claude/settings.json CLAUDE.local.md -.cursor/chat/ -.cursor/composer/ -.cursor/mcp.json +.cursor/ .windsurf/ .cline/ .continue/ @@ -111,4 +105,4 @@ CLAUDE.local.md .aider* .codex/ .codeium/ -.copilot/ +.copilot/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index ce8776c..095f347 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ Nested objects that don't have their own API endpoint extend `DataObject` instea ## Testing -Stack: unittest + requests_mock. Tests live in `test/api/test_.py`. CI runs on GitHub Actions with Python 3.10-3.14. +Stack: unittest + requests_mock. Tests live in `test/api/test_.py`. Test pattern: 1. Define fixture dicts at module level matching API JSON responses From 42f32c283f30c563356ed79bed75c4a3105d4d94 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 14:37:47 +0200 Subject: [PATCH 6/8] Restore granular .cursor/ ignores instead of ignoring whole dir .cursor/rules/ can be committed for shared project rules, so only ignore ephemeral subdirs (chat/, composer/) and local config (mcp.json). --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cb2a0ff..af46eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,9 @@ ENV/ !.claude/hooks/ !.claude/settings.json CLAUDE.local.md -.cursor/ +.cursor/chat/ +.cursor/composer/ +.cursor/mcp.json .windsurf/ .cline/ .continue/ From 0689a713169f87d51f304f2fad82e71d899e3f69 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 14:49:45 +0200 Subject: [PATCH 7/8] Scope gh permissions to read-only operations Replace broad Bash(gh pr:*) with granular read-only rules: - gh pr view/list/diff/checks/status - gh run view/list/watch - gh issue view/list - gh release view/list - gh repo view - gh search Write operations (gh pr create/edit/merge, gh api POST) will now prompt for confirmation instead of running automatically. --- .claude/settings.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.claude/settings.json b/.claude/settings.json index 9fb0155..cd37dbd 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,7 +21,20 @@ "Bash(git pull:*)", "Bash(git cherry-pick:*)", "Bash(git reset:*)", - "Bash(gh pr:*)" + "Bash(gh pr view:*)", + "Bash(gh pr list:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr checks:*)", + "Bash(gh pr status:*)", + "Bash(gh run view:*)", + "Bash(gh run list:*)", + "Bash(gh run watch:*)", + "Bash(gh issue view:*)", + "Bash(gh issue list:*)", + "Bash(gh release view:*)", + "Bash(gh release list:*)", + "Bash(gh repo view:*)", + "Bash(gh search:*)" ], "deny": [ "Read(./.env*)" From 76840f37defaabd2519ff7c3d9169367bd5bf63e Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 15:08:36 +0200 Subject: [PATCH 8/8] Use built-in session_id and project-local tracker path - session-start-setup.sh: read session_id from hook input JSON instead of generating a custom one - Move file tracker from /tmp to .claude/tmp/ (already gitignored by .claude/* rule) so hooks don't write outside the project directory --- .claude/hooks/post-edit-lint.sh | 3 ++- .claude/hooks/post-stop-validate.sh | 2 +- .claude/hooks/session-start-setup.sh | 12 +++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh index 8beec39..b1a972a 100755 --- a/.claude/hooks/post-edit-lint.sh +++ b/.claude/hooks/post-edit-lint.sh @@ -8,7 +8,8 @@ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only track .py files, skip vendored/generated paths if [[ "$FILE" == *.py ]] && [[ "$FILE" != *__pycache__* ]] && [[ "$FILE" != *.egg-info* ]] && [[ "$FILE" != */dist/* ]] && [[ "$FILE" != */build/* ]]; then - TRACKER="/tmp/claude-edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" + TRACKER="$CLAUDE_PROJECT_DIR/.claude/tmp/edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" + mkdir -p "$CLAUDE_PROJECT_DIR/.claude/tmp" echo "$FILE" >> "$TRACKER" sort -u "$TRACKER" -o "$TRACKER" fi diff --git a/.claude/hooks/post-stop-validate.sh b/.claude/hooks/post-stop-validate.sh index 45384c3..59648b5 100755 --- a/.claude/hooks/post-stop-validate.sh +++ b/.claude/hooks/post-stop-validate.sh @@ -3,7 +3,7 @@ # Python has no autoformatter configured, so lint + tests only. cd "$CLAUDE_PROJECT_DIR" || exit 0 -TRACKER="/tmp/claude-edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" +TRACKER="$CLAUDE_PROJECT_DIR/.claude/tmp/edited-py-files-${CLAUDE_HOOK_SESSION_ID:-default}" ctx="" diff --git a/.claude/hooks/session-start-setup.sh b/.claude/hooks/session-start-setup.sh index 387022e..c2e9e9e 100755 --- a/.claude/hooks/session-start-setup.sh +++ b/.claude/hooks/session-start-setup.sh @@ -1,11 +1,13 @@ #!/bin/bash -# SessionStart: generate session ID, detect repo state. Must be <1s, no network. +# SessionStart: persist session ID, detect repo state. Must be <1s, no network. cd "$CLAUDE_PROJECT_DIR" || exit 0 -# Generate a stable session ID and persist via CLAUDE_ENV_FILE -# so the edit tracker and stop hook share the same file path -SESSION_ID="$(date +%s)-$$" -if [[ -n "$CLAUDE_ENV_FILE" ]]; then +INPUT=$(cat) +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') + +# Persist session ID via CLAUDE_ENV_FILE so the edit tracker +# and stop hook share the same file path +if [[ -n "$CLAUDE_ENV_FILE" ]] && [[ -n "$SESSION_ID" ]]; then echo "export CLAUDE_HOOK_SESSION_ID='$SESSION_ID'" >> "$CLAUDE_ENV_FILE" fi