diff --git a/.claude/README.md b/.claude/README.md index cdc3aef..beadc3e 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -4,21 +4,17 @@ This directory was automatically created by Git template when you initialized or ## What is this? -The `.claude/` directory provides project-specific configuration and extensions for Claude Code CLI (CCCLI) collaboration. It integrates with global infrastructure at `~/.claude/` to provide: +The `.claude/` directory provides project-specific configuration for Claude Code CLI (CCCLI) collaboration. It integrates with global infrastructure at `~/.claude/` to provide: - Project-specific configuration (Node version, required tools, deployment secrets) -- Custom git hook extensions for project-specific validation - Project-local documentation and patterns ## Directory Structure -``` +```text .claude/ ├── README.md # This file -├── config.sh.template # Template for project configuration -└── hooks/ - └── extensions/ # Project-specific git hook extensions - └── example.sh.disabled # Example extension (disabled by default) +└── config.sh.template # Template for project configuration ``` ## Quick Start @@ -64,34 +60,21 @@ If you need project-specific settings: ### Option 3: Add Custom Hook Extensions -If you need project-specific validation: - -1. Create a new file in `.claude/hooks/extensions/`: +Project-specific git hook extensions do **not** live in `.claude/`. They live in +`.project-hooks/` at the repository root: - ```bash - touch .claude/hooks/extensions/my-validation.sh - chmod +x .claude/hooks/extensions/my-validation.sh - ``` +- `.project-hooks/pre-commit` — runs after the global lint pass, before the AI review +- `.project-hooks/pre-push` — same seam for push-time checks -2. Write your validation logic: +Both must be executable (`chmod +x`). The trust model is the same as any build +tooling: if you cloned the repo and are committing to it, you trust its scripts. - ```bash - #!/usr/bin/env bash - # Extension contract: - # - Exit 0: Check passed (allow git operation) - # - Exit 1: Check failed (block git operation) - # - Can use functions from ~/.claude/hooks/lib/hook-common.sh - - # Your validation logic here - if [[ condition_fails ]]; then - echo "ERROR: Validation failed" - exit 1 - fi - - exit 0 - ``` +```bash +touch .project-hooks/pre-commit +chmod +x .project-hooks/pre-commit +``` -3. Extensions run automatically on relevant git operations (commit, push, etc.) +The contract is exit 0 to allow the git operation, exit 1 to block it. ## Common Patterns @@ -116,7 +99,7 @@ export DEPLOYMENT_REQUIRED_SECRETS=( ### Custom Security Check ```bash -# .claude/hooks/extensions/security.sh +# .project-hooks/pre-commit #!/usr/bin/env bash # Block commits with hardcoded API keys @@ -130,7 +113,9 @@ exit 0 ## Integration with Global Infrastructure -Global hooks at `~/.config/git/hooks/` automatically discover and run extensions in this directory. No configuration needed - just add your `.sh` files and make them executable. +Global hooks at `~/.config/git/hooks/` run the project-local extensions in +`.project-hooks/`. Nothing executes files inside `.claude/` — that directory is +configuration and documentation only. **Global Infrastructure Documentation**: `~/.claude/docs/INFRASTRUCTURE.md` @@ -140,16 +125,6 @@ Global hooks at `~/.config/git/hooks/` automatically discover and run extensions Template for project configuration. Copy to `config.sh` and customize with your project's requirements. -### hooks/extensions/example.sh.disabled - -Example extension showing the basic structure. Disabled by default (`.disabled` suffix prevents execution). - -To enable: - -1. Remove `.disabled` suffix: `mv example.sh.disabled my-check.sh` -2. Customize validation logic -3. Ensure executable: `chmod +x .claude/hooks/extensions/my-check.sh` - ## Next Steps 1. **Review your needs**: Do you need project-specific configuration or validation? @@ -168,11 +143,11 @@ To enable: ### Extensions not running? ```bash -# Check extensions are executable -ls -la .claude/hooks/extensions/ +# Check the extension exists at the right path and is executable +ls -la .project-hooks/ # Make executable if needed -chmod +x .claude/hooks/extensions/*.sh +chmod +x .project-hooks/pre-commit .project-hooks/pre-push ``` ### Config not being used? diff --git a/.claude/hooks/extensions/example.sh.disabled b/.claude/hooks/extensions/example.sh.disabled deleted file mode 100644 index 57a9794..0000000 --- a/.claude/hooks/extensions/example.sh.disabled +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env bash -# Example Git Hook Extension (DISABLED by default) -# -# This file demonstrates how to create project-specific git hook extensions. -# Extensions are discovered and executed by global hooks automatically. -# -# TO ENABLE THIS EXTENSION: -# 1. Rename to remove .disabled suffix: -# mv example.sh.disabled my-validation.sh -# 2. Customize the validation logic below, and turn on the checks you want -# by setting the matching ENABLE_* variable to 1 in main() (all default -# to 0, so an unmodified copy of this file runs no checks) -# 3. Ensure it's executable: -# chmod +x .claude/hooks/extensions/my-validation.sh -# -# EXTENSION CONTRACT: -# - Exit 0: Check passed (allow git operation to proceed) -# - Exit 1: Check failed (block git operation) -# - Can use functions from ~/.claude/hooks/lib/hook-common.sh -# - Receives same arguments as parent hook (e.g., commit message file for commit-msg hook) -# -# AVAILABLE FUNCTIONS (from hook-common.sh): -# log_info "message" - Blue informational message -# log_success "message" - Green success message -# log_warn "message" - Yellow warning message -# log_error "message" - Red error message -# get_staged_files - Get list of staged files -# get_repo_root - Get repository root directory -# is_protected_branch - Check if on main/master branch - -# ============================================ -# EXAMPLE 1: Block commits during business hours -# ============================================ - -check_business_hours() { - local current_hour - current_hour=$(date +%H) - - # Check if current time is during business hours (9 AM - 5 PM) - if [[ ${current_hour} -ge 9 && ${current_hour} -lt 17 ]]; then - log_warn "⏰ Commit during business hours detected" - - # Check if commit message includes ticket reference - local commit_msg_file="$1" - if [[ -n "${commit_msg_file}" ]] && [[ -f "${commit_msg_file}" ]]; then - if ! grep -qE '(JIRA|TICKET|#)[- ]?[0-9]+' "${commit_msg_file}"; then - log_error "❌ Commits during business hours must reference a ticket" - echo " Format: JIRA-123, TICKET-456, or #789" - return 1 - fi - fi - fi - - return 0 -} - -# ============================================ -# EXAMPLE 2: Check for TODO comments without issue references -# ============================================ - -check_todo_comments() { - # Get staged changes - local staged_changes - staged_changes=$(git diff --cached) - - # Look for TODO comments without issue references - # Pattern: TODO without a # followed by digits (grep -E has no lookahead, - # so filter added lines containing TODO, then exclude ones with "TODO #N") - local todo_lines - todo_lines=$(echo "${staged_changes}" | grep -iE '^\+.*TODO' | grep -vE 'TODO #[0-9]' || true) - if [[ -n "${todo_lines}" ]]; then - log_warn "⚠️ TODO comment without issue reference detected" - echo "" - echo "Found TODO comments that don't reference an issue:" - echo "${todo_lines}" | sed 's/^/ /' - echo "" - echo "Please use format: TODO #123 (with GitHub issue number)" - return 1 - fi - - return 0 -} - -# ============================================ -# EXAMPLE 3: Prevent hardcoded secrets -# ============================================ - -check_hardcoded_secrets() { - # Get staged changes - local staged_changes - staged_changes=$(git diff --cached) - - # Check for common secret patterns - local secret_patterns=( - "api[_-]?key.*=.*[\"'][a-zA-Z0-9]{32,}" - "secret[_-]?key.*=.*[\"'][a-zA-Z0-9]{32,}" - "password.*=.*[\"'][^\"']{8,}" - "token.*=.*[\"'][a-zA-Z0-9]{32,}" - ) - - for pattern in "${secret_patterns[@]}"; do - if echo "${staged_changes}" | grep -iE "^\+.*${pattern}"; then - log_error "❌ Potential hardcoded secret detected" - echo "" - echo "Pattern matched: ${pattern}" - echo "" - echo "Please use environment variables or secret management instead." - return 1 - fi - done - - return 0 -} - -# ============================================ -# EXAMPLE 4: Enforce code formatting -# ============================================ - -check_formatting() { - # Get list of staged files - local staged_files - staged_files=$(git diff --cached --name-only --diff-filter=ACM) - - # Check if prettier is available - if ! command -v prettier &>/dev/null; then - log_warn "⚠️ Prettier not found, skipping format check" - return 0 - fi - - # Check JavaScript/TypeScript files - local js_files - js_files=$(echo "${staged_files}" | grep -E '\.(js|jsx|ts|tsx)$' || true) - - if [[ -n "${js_files}" ]]; then - local unformatted_files - unformatted_files=$(echo "${js_files}" | xargs prettier --check 2>&1 | grep -E '^/' || true) - - if [[ -n "${unformatted_files}" ]]; then - log_error "❌ Unformatted files detected" - echo "" - echo "The following files are not formatted:" - echo "${unformatted_files}" | sed 's/^/ /' - echo "" - echo "Run: prettier --write " - return 1 - fi - fi - - return 0 -} - -# ============================================ -# EXAMPLE 5: Validate commit message format -# ============================================ - -check_commit_message_format() { - local commit_msg_file="$1" - - if [[ -z "${commit_msg_file}" ]] || [[ ! -f "${commit_msg_file}" ]]; then - # Not a commit message hook call - return 0 - fi - - local commit_msg - commit_msg=$(cat "${commit_msg_file}") - - # Skip merge commits - if [[ "${commit_msg}" =~ ^Merge ]]; then - return 0 - fi - - # Check conventional commit format: type(scope): subject - if ! echo "${commit_msg}" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\([a-z0-9-]+\))?: .+'; then - log_error "❌ Commit message does not follow conventional format" - echo "" - echo "Current message:" - echo " ${commit_msg}" - echo "" - echo "Expected format:" - echo " type(scope): subject" - echo "" - echo "Types: feat, fix, docs, style, refactor, test, chore" - echo "Example: feat(auth): add JWT token refresh" - return 1 - fi - - return 0 -} - -# ============================================ -# MAIN EXECUTION -# ============================================ - -main() { - # Every check is OFF by default. Enable the ones you want by setting the - # matching variable to 1 — either here (change the default) or in the - # environment. Enabling a check makes it block the git operation on - # failure, exactly as the extension contract above describes. - - [[ "${ENABLE_BUSINESS_HOURS:-0}" == 1 ]] && { check_business_hours "$@" || exit 1; } - [[ "${ENABLE_TODO_COMMENTS:-0}" == 1 ]] && { check_todo_comments || exit 1; } - [[ "${ENABLE_HARDCODED_SECRETS:-0}" == 1 ]] && { check_hardcoded_secrets || exit 1; } - [[ "${ENABLE_FORMATTING:-0}" == 1 ]] && { check_formatting || exit 1; } - [[ "${ENABLE_COMMIT_MESSAGE_FORMAT:-0}" == 1 ]] && { check_commit_message_format "$@" || exit 1; } - - # If all checks pass (or none are enabled) - log_success "✅ Example validation passed" - exit 0 -} - -# Run main function -main "$@"