diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000..4cbf825 --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,7 @@ +# `.claude/` — federated harness surface + +Federated under [terrarium](https://github.com/cerebral-work/terrarium) standards. + +| Hook | Event | What | Bypass | +|---|---|---|---| +| `guard-main-push` | PreToolUse Bash | deny direct push to main | `# allow-direct-push` | diff --git a/.claude/hooks/guard-main-push.sh b/.claude/hooks/guard-main-push.sh new file mode 100755 index 0000000..957559f --- /dev/null +++ b/.claude/hooks/guard-main-push.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Deny direct push to main/master. Override: "# allow-direct-push". +input=$(cat); cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""') +case "$cmd" in *"# allow-direct-push"*) exit 0 ;; esac +if printf '%s' "$cmd" | grep -qE 'git +push.*(origin +)?(main|master)\b' \ + || { printf '%s' "$cmd" | grep -qE 'git +push' && [ "$(git branch --show-current 2>/dev/null)" = "main" ]; }; then + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Direct push to main is blocked — open a PR. Override: # allow-direct-push and surface why."}}' +fi +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..567c944 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "attribution": { "commit": "", "pr": "" }, + "permissions": { + "allow": [ + "Bash(git add *)", "Bash(git commit *)", "Bash(git push)", + "Bash(git fetch *)", "Bash(git status*)", "Bash(git diff*)", + "Bash(git log*)", "Bash(git checkout *)", + "Bash(rg *)", "Bash(grep *)", "Bash(jq *)", "Bash(gh pr *)", + "Read", "Edit", "Write" + ], + "deny": ["Bash(rm -rf /*)", "Bash(rm -rf ~)"], + "defaultMode": "default" + }, + "hooks": { + "PreToolUse": [ + { "matcher": "Bash", "hooks": [ + { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-main-push.sh", "timeout": 5 } + ]} + ] + } +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..194873f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @todie +/.claude/ @todie +/.github/ @todie diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4ae149 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: gitleaks + env: + GITLEAKS_VERSION: 8.30.1 + GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + shell: bash + run: | + set -euo pipefail + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o gitleaks.tar.gz + echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c - + tar -xzf gitleaks.tar.gz gitleaks + ./gitleaks git . --redact --no-banner --verbose diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..4575d5b --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,14 @@ +# Conventional Commits + gitleaks (federated standard). +# Skip: LEFTHOOK=0. +commit-msg: + commands: + conventional: + run: | + head -1 {1} | grep -qE '^(feat|fix|perf|refactor|docs|test|ci|build|infra|security|style|chore)(\(.+\))?!?: .+' \ + || { echo "commit-msg must be Conventional Commits: (scope): "; exit 1; } + +pre-commit: + parallel: true + commands: + gitleaks: + run: gitleaks protect --staged --redact --no-banner || echo "gitleaks not installed — CI will enforce"