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:
parent
9a77b5921d
commit
76f719e760
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}"
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -5,7 +5,24 @@
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(cat:*)",
|
"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": [
|
"deny": [
|
||||||
"Read(*/.env*)",
|
"Read(*/.env*)",
|
||||||
|
|
@ -14,10 +31,39 @@
|
||||||
"Write(*/secrets/*)",
|
"Write(*/secrets/*)",
|
||||||
"Edit(*/.env*)",
|
"Edit(*/.env*)",
|
||||||
"Edit(*/secrets/*)",
|
"Edit(*/secrets/*)",
|
||||||
|
"Read(*/uploads/*)",
|
||||||
|
"Write(*/uploads/*)",
|
||||||
|
"Edit(*/uploads/*)",
|
||||||
|
"Read(*/.codegraph/*)",
|
||||||
|
"Write(*/.codegraph/*)",
|
||||||
|
"Edit(*/.codegraph/*)",
|
||||||
"Bash(rm -f*)",
|
"Bash(rm -f*)",
|
||||||
"Bash(rm -rf*)",
|
"Bash(rm -rf*)",
|
||||||
"Bash(git push -f*)",
|
"Bash(git push -f*)",
|
||||||
"Bash(git push --force*)"
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue