chore(claude): expand permissions and add session/env-guard hooks (Step 2)

Permissions:
- Allow npm run/test/install, uv run/sync, docker (compose), and the
  common read-only/staging git commands so routine work doesn't trigger
  permission prompts.
- Deny Read/Write/Edit on uploads/ and .codegraph/ (auto-generated and
  user-data paths) in addition to the existing .env*/secrets/ blocks.

Hooks:
- SessionStart: print branch, ahead/behind vs upstream, and working-tree
  state at session start so context is visible immediately.
- PreToolUse (Read|Write|Edit|Bash|NotebookEdit): defence-in-depth
  guard that intercepts attempts to access .env / secrets/ paths (and
  bash commands targeting them) with a friendly, logged refusal on top
  of the permissions.deny rules.

PostToolUse formatter is intentionally skipped — the project has no
configured formatter (per the Step 1 conventions decision).
The Stop hook (quality gate) will be configured in Step 6.

Documentation: .claude/onboarding/step2_setup/01_settings_analysis.md
This commit is contained in:
Dominik Seemann 2026-05-06 17:44:16 +02:00
parent 9a77b5921d
commit 76f719e760
5 changed files with 221 additions and 1 deletions

View File

@ -0,0 +1,38 @@
"""Helper for pre_tool_env_guard.sh — reads tool-call JSON from stdin and
prints a "match" line if the call would touch .env or secrets/. Empty
output means no match (allow)."""
import json
import re
import sys
def main() -> None:
try:
data = json.load(sys.stdin)
except Exception:
return
ti = data.get("tool_input", {}) or {}
fp = (
ti.get("file_path", "")
or ti.get("path", "")
or ti.get("notebook_path", "")
)
cmd = ti.get("command", "") or ""
path_pattern = re.compile(r"(^|/)(\.env(\.|$)|secrets/)")
cmd_pattern = re.compile(
r"(^|[ \t;|&])\s*(cat|less|more|head|tail|cp|mv|rm)\s+"
r"[^|;&]*(?:\.env|secrets/)"
)
if fp and path_pattern.search(fp):
print(f"path:{fp}")
return
if cmd and cmd_pattern.search(cmd):
print(f"command:{cmd[:120]}")
return
if __name__ == "__main__":
main()

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# PreToolUse hook — extra explicit refusal for any attempt to touch
# .env / secrets paths, on top of the permissions.deny rules in
# .claude/settings.json. Provides a clearer, friendlier message and a
# log line.
#
# Receives the tool-call payload as JSON on stdin:
# { "tool_name": "Read|Write|Edit|Bash|...",
# "tool_input": { "file_path": "...", "command": "..." } }
#
# Exit codes:
# 0 → allow (silent)
# 2 → block; stderr is shown to Claude so it knows why
set -euo pipefail
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
matches="$(python3 "$HOOK_DIR/_env_guard.py" 2>/dev/null || true)"
if [ -n "$matches" ]; then
echo "🚫 Blocked: attempt to access protected path (env / secrets)." >&2
echo " Detail: $matches" >&2
echo " Reason: .env files and secrets/ are off-limits to Claude in this project." >&2
echo " To grant a one-off exception, ask the developer to read the file and paste the relevant value." >&2
exit 2
fi
exit 0

33
.claude/hooks/session_start.sh Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# SessionStart hook — print branch + working-tree status to give Claude
# (and the developer) immediate context at the start of a session.
set -euo pipefail
# Run only inside a git repo
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 0
fi
branch="$(git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo '')"
# Ahead / behind vs upstream
ahead_behind=""
if [ -n "$upstream" ]; then
counts="$(git rev-list --left-right --count "${upstream}...HEAD" 2>/dev/null || echo '0 0')"
behind="$(echo "$counts" | awk '{print $1}')"
ahead="$(echo "$counts" | awk '{print $2}')"
if [ "$ahead" != "0" ] || [ "$behind" != "0" ]; then
ahead_behind=" (ahead $ahead, behind $behind vs $upstream)"
fi
fi
# Working-tree status
dirty_count="$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
if [ "$dirty_count" = "0" ]; then
state="clean"
else
state="$dirty_count uncommitted change(s)"
fi
echo "📍 Branch: ${branch}${ahead_behind}${state}"

View File

@ -0,0 +1,74 @@
# Step 2 — Settings & Hooks Analysis
Decisions made while configuring `.claude/settings.json` and the hooks
(Step 2).
## Date: 2026-05-06
## Project Context (PHASE 1 findings)
- **Backend:** Python (Flask, `uv`). No Ruff / Black / pre-commit config.
- **Frontend:** Vue 3 + Vite. No Prettier / ESLint config.
- **Sensitive paths:** `.env`, `.env.example`. No `secrets/` folder
exists today; rule blocks the path proactively. User uploads land in
`backend/uploads/`. CodeGraph index lives in `.codegraph/`.
- **Project decision (Step 1):** No enforced formatter — match the
surrounding file's style.
## Decisions
| Question | Decision |
|----------|----------|
| Q1 — Allow more bash? | **Yes.** Added `npm run:*`, `npm test:*`, `npm install:*`, `git status`, `git diff:*`, `git log:*`, `git show:*`, `git add:*`, `git branch:*`, `git checkout:*`, `git commit:*`, `git restore:*`, `uv run:*`, `uv sync:*`, `docker compose:*`, `docker-compose:*`. |
| Q2 — Deny additions? | **Yes.** Added Read/Write/Edit denials for `*/uploads/*` and `*/.codegraph/*`. |
| Q3 — PostToolUse formatter? | **Skipped.** No formatter configured in this project. Matches the established convention (Step 1 — "no enforced formatter at present"). Add later if/when the team adopts one. |
| Q4 — PreToolUse `.env` guard hook? | **Added** as a friendly, logged refusal layered on top of the `permissions.deny` rules. |
| Q5 — SessionStart hook? | **Both** — branch + status. Single line: `📍 Branch: <branch> (ahead N, behind M vs upstream) — clean / N uncommitted change(s)`. |
| Q6 — `.gitignore` for `settings.local.json`? | **Already done in Step 0**`.claude/settings.local.json` and `.claude/.credentials.json` are ignored. |
## What Was Created / Updated
### `.claude/settings.json`
- **Permissions:**
- `allow` — safe nav (`cd`, `ls`, `find`, `cat`, `mkdir`), `npm` /
`uv` task running, common read-only and staging git commands,
docker-compose for the recommended deployment path.
- `deny``.env*`, `secrets/`, `uploads/`, `.codegraph/` (Read /
Write / Edit), destructive bash (`rm -f*`, `rm -rf*`,
`git push -f*`, `git push --force*`).
- **Hooks:**
- `SessionStart``.claude/hooks/session_start.sh`
- `PreToolUse` (matcher: `Read|Write|Edit|Bash|NotebookEdit`) →
`.claude/hooks/pre_tool_env_guard.sh`
### `.claude/hooks/session_start.sh`
- Prints branch + ahead/behind vs. upstream + working-tree state
(clean / N uncommitted changes) on session start.
- Silent exit when not inside a git repo.
### `.claude/hooks/pre_tool_env_guard.sh` + `_env_guard.py`
- Defence-in-depth on top of `permissions.deny`.
- Inspects `tool_input.file_path` and `tool_input.command` for
`.env*` / `secrets/` references.
- Blocks (`exit 2`) with a clear, friendly stderr message:
- what was blocked (path or command excerpt)
- why (project policy)
- how to grant a one-off exception (developer copy-pastes the
relevant value)
- Tested against positive and negative inputs (Read on `/foo/.env`,
bash `cat /foo/.env`, plain `ls`, plain file Read).
### Stop hook
- **Not configured here** — Step 6 (Quality & Review) covers the
full quality-gate Stop hook. Out of scope for Step 2.
## Test Results (PHASE 6 verification)
- `session_start.sh` prints the expected single-line summary.
- `pre_tool_env_guard.sh`:
- Read `/foo/.env` → blocked, exit 2 ✓
- Read `/foo/safe.txt` → allowed, exit 0 ✓
- Bash `cat /foo/.env` → blocked, exit 2 ✓
- Bash `ls` → allowed, exit 0 ✓
- `settings.json` parses as valid JSON ✓
## Next
- Step 3: Planning Tool Integration (MCP server, sprint context).

View File

@ -5,7 +5,24 @@
"Bash(ls:*)",
"Bash(find:*)",
"Bash(cat:*)",
"Bash(mkdir:*)"
"Bash(mkdir:*)",
"Bash(npm run:*)",
"Bash(npm test:*)",
"Bash(npm install:*)",
"Bash(git status)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git add:*)",
"Bash(git branch:*)",
"Bash(git checkout:*)",
"Bash(git commit:*)",
"Bash(git restore:*)",
"Bash(uv run:*)",
"Bash(uv sync:*)",
"Bash(docker compose:*)",
"Bash(docker-compose:*)"
],
"deny": [
"Read(*/.env*)",
@ -14,10 +31,39 @@
"Write(*/secrets/*)",
"Edit(*/.env*)",
"Edit(*/secrets/*)",
"Read(*/uploads/*)",
"Write(*/uploads/*)",
"Edit(*/uploads/*)",
"Read(*/.codegraph/*)",
"Write(*/.codegraph/*)",
"Edit(*/.codegraph/*)",
"Bash(rm -f*)",
"Bash(rm -rf*)",
"Bash(git push -f*)",
"Bash(git push --force*)"
]
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session_start.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Read|Write|Edit|Bash|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre_tool_env_guard.sh"
}
]
}
]
}
}