diff --git a/.claude/hooks/block-credential-edit.sh b/.claude/hooks/block-credential-edit.sh new file mode 100755 index 0000000..5e49834 --- /dev/null +++ b/.claude/hooks/block-credential-edit.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# PreToolUse hook: block direct edits to credential files. +# Only .example templates should be modified — real secrets stay untouched. + +set -euo pipefail + +input=$(cat) +file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty') + +# No file path in input (e.g. Bash tool) — allow +[[ -z "$file_path" ]] && exit 0 + +basename=$(basename "$file_path") + +# Block known credential files (but allow .example templates) +case "$basename" in + 99-claude|99-gemini|99-google|99-huggingface|99-replicate) + echo "Blocked: do not edit credential files directly — edit the .example template instead" >&2 + exit 2 + ;; +esac + +exit 0 diff --git a/.claude/hooks/shellcheck-on-edit.sh b/.claude/hooks/shellcheck-on-edit.sh new file mode 100755 index 0000000..92eaff4 --- /dev/null +++ b/.claude/hooks/shellcheck-on-edit.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# PostToolUse hook: run shellcheck after editing a bash.d script. +# Skips non-script files (markdown, .gitignore, etc). + +set -euo pipefail + +input=$(cat) +file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty') + +# No file path — nothing to check +[[ -z "$file_path" ]] && exit 0 + +basename=$(basename "$file_path") + +# Only check files that match the numbered script naming convention +# or .example templates (which are also valid shell scripts) +case "$basename" in + [0-9][0-9]-*|[0-9][0-9]-*.example) ;; + *) exit 0 ;; +esac + +# File must still exist (Write could have been to a new path) +[[ -f "$file_path" ]] || exit 0 + +# Run shellcheck — exit 2 feeds stderr back to Claude +if ! shellcheck "$file_path" 2>&1; then + echo "shellcheck failed on $basename — please fix the issues above" >&2 + exit 2 +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..b508082 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-credential-edit.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/shellcheck-on-edit.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/new-credential/SKILL.md b/.claude/skills/new-credential/SKILL.md new file mode 100644 index 0000000..8b4eb6a --- /dev/null +++ b/.claude/skills/new-credential/SKILL.md @@ -0,0 +1,50 @@ +--- +name: new-credential +description: Create a new bash.d credential file with .example template, .gitignore entry, and correct permissions +user-invocable: true +disable-model-invocation: true +arguments: + - name: name + description: "Short name for the credential (e.g. 'openai', 'stripe')" + required: true + - name: var + description: "Environment variable name to export (e.g. 'OPENAI_API_KEY')" + required: true +--- + +Create a new credential file pair in ~/.bash.d/ following the project conventions. + +## Steps + +1. **Create the `.example` template** at `99-$name.example` (mode 644): + ```bash + # shellcheck shell=bash + # + # Copy to 99-$name and fill in your token, then: chmod 700 99-$name + require_private "${BASH_SOURCE[0]}" + export $var=your-token-here + ``` + +2. **Create the real credential file** at `99-$name` (mode 700): + ```bash + # shellcheck shell=bash + # NOTE: Contains credentials - ensure file permissions remain 600/700 + require_private "${BASH_SOURCE[0]}" + export $var=your-token-here + ``` + +3. **Add `99-$name` to `.gitignore`** (append to the existing credential list) + +4. **Set permissions**: `chmod 700 99-$name` + +5. **Validate both files**: `shellcheck 99-$name.example 99-$name` + +6. **Remind the user** to edit `99-$name` and fill in the real secret value + +## Rules + +- The `.example` template must NOT contain real secrets — use `your-token-here` as placeholder +- The real credential file must have mode `700` +- Both files must start with `# shellcheck shell=bash` +- Both files must call `require_private "${BASH_SOURCE[0]}"` as the first functional line +- Only the `.example` file should be staged in git — verify `99-$name` is gitignored