diff --git a/.agents/evals/cross-cutting/gitleaks/eval.yaml b/.agents/evals/cross-cutting/gitleaks/eval.yaml
new file mode 100644
index 0000000..7f7f05e
--- /dev/null
+++ b/.agents/evals/cross-cutting/gitleaks/eval.yaml
@@ -0,0 +1,84 @@
+skill_name: gitleaks
+
+trigger_tests:
+ - id: explicit-trigger-install
+ name: Explicit trigger — install and configure
+ query: "set up gitleaks in this repo"
+ should_trigger: true
+
+ - id: explicit-trigger-update-hook
+ name: Explicit trigger — update hook
+ query: "update the gitleaks pre-commit hook"
+ should_trigger: true
+
+ - id: implicit-trigger-false-positive
+ name: Implicit trigger — suppress false positive in pre-commit hook
+ query: "my pre-commit hook keeps blocking commits because it thinks my test fixture has an API key, how do I suppress it?"
+ should_trigger: true
+
+ - id: implicit-trigger-scan-history
+ name: Implicit trigger — audit repo history for secrets
+ query: "I want to scan my entire git history to make sure no credentials were ever committed"
+ should_trigger: true
+
+ - id: negative-trigger-security-review
+ name: Negative trigger — general code security review
+ query: "do a security review of this pull request"
+ should_trigger: false
+
+ - id: negative-trigger-ci-scan
+ name: Negative trigger — CI pipeline secret scanning
+ query: "add secret scanning to my GitHub Actions CI pipeline"
+ should_trigger: false
+
+ - id: negative-trigger-other-tool
+ name: Negative trigger — different secret scanning tool
+ query: "trufflehog is flagging false positives in my repo, how do I configure it?"
+ should_trigger: false
+
+output_tests:
+ - id: output-states-intent-before-running
+ name: Deterministic — states intent and waits for confirmation before running setup script
+ type: deterministic
+ prompt: "set up gitleaks in /home/user/myproject"
+ expected_output: >
+ The skill states what it is about to do (run setup-gitleaks.sh, which will modify
+ .git/hooks/pre-commit and seed .gitleaks.toml) and asks for confirmation before
+ executing the script. The confirmation request appears before any Bash command
+ that runs the script.
+ assertions:
+ - "Output contains 'setup-gitleaks.sh' before any Bash tool invocation that runs it"
+ - "Output contains a confirmation prompt (e.g. 'Proceed?') before running the setup script"
+ - "Output does not run the setup script without an explicit confirmation step"
+
+ - id: output-redacts-secret-values
+ name: Deterministic — redacts secret value and prescribes rotation before history rewrite
+ type: deterministic
+ prompt: "gitleaks found an AWS secret key on line 42 of config/settings.py — it says the value is AKIAIOSFODNN7EXAMPLE. What do I do?"
+ expected_output: >
+ The skill does not echo or repeat the secret value. It references the finding by
+ rule ID, file path, and line number only. It states that the secret must be rotated
+ immediately. It mentions git filter-repo (not git filter-branch) for history rewrite
+ and asks for user confirmation before running the rewrite.
+ assertions:
+ - "Output does not contain the literal string 'AKIAIOSFODNN7EXAMPLE'"
+ - "Output mentions rotating or revoking the credential as the first action"
+ - "Output references 'git filter-repo' for history rewrite, not 'git filter-branch'"
+ - "Output states that user confirmation is required before running the history rewrite"
+
+ - id: output-quality-allowlist-guidance
+ name: LLM-rubric — allowlist guidance is correct, version-aware, and minimal
+ type: llm-rubric
+ prompt: "gitleaks keeps flagging my docs/research/ directory as containing secrets, how do I suppress it?"
+ expected_output: >
+ High-quality output checks the installed gitleaks version before prescribing any
+ TOML syntax, recommends a path-based allowlist entry in .gitleaks.toml (not a
+ .gitleaksignore fingerprint), uses the correct TOML syntax for the detected version,
+ adds only the minimal allowlist entry needed for the identified false positive, and
+ includes a verification step (re-run gitleaks dir -v or gitleaks dir --log-level debug)
+ after making the change.
+ assertions:
+ - "Output checks or asks about the gitleaks version before writing TOML syntax"
+ - "Output recommends a path-based allowlist entry in .gitleaks.toml rather than .gitleaksignore"
+ - "Output includes a command to verify the suppression works after the change"
+ - "Output explains why .gitleaksignore fingerprints are fragile (line numbers shift)"
diff --git a/.agents/skills/gitleaks/META.md b/.agents/skills/gitleaks/META.md
new file mode 100644
index 0000000..ee41cf2
--- /dev/null
+++ b/.agents/skills/gitleaks/META.md
@@ -0,0 +1,15 @@
+```yaml
+version: "1.0"
+updated: 2026-06-20
+
+when: >
+ Invoked when the user wants to install gitleaks and wire it as a git pre-commit secret
+ scanner, update the hook in an existing repo, tune allowlist rules to suppress false
+ positives, debug a scan finding, or rotate a real secret that was found. Covers the full
+ lifecycle: install → configure → maintain → remediate. Not invoked for general code
+ security review (security-review skill) or CI pipeline secret scanning (write-ci-pipeline skill).
+
+references:
+ - https://github.com/gitleaks/gitleaks/releases/tag/v8.24.2
+ - https://github.com/gitleaks/gitleaks/blob/main/README.md
+```
diff --git a/.agents/skills/gitleaks/SKILL.md b/.agents/skills/gitleaks/SKILL.md
new file mode 100644
index 0000000..4e5d5b9
--- /dev/null
+++ b/.agents/skills/gitleaks/SKILL.md
@@ -0,0 +1,111 @@
+---
+name: gitleaks
+description: Use when the user wants to install gitleaks, wire it as a git pre-commit secret scanner, update the hook in an existing repo, tune allowlist rules, resolve false positives, or debug a gitleaks scan finding. Do NOT use when the user wants a general security review of code (use security-review), wants to add secret scanning to a CI pipeline (use write-ci-pipeline), or is asking about a different secret scanning tool such as trufflehog or git-secrets.
+metadata:
+ category: cross-cutting
+allowed-tools:
+ - Bash
+ - Read
+ - Edit
+---
+
+
+
+## Required inputs
+
+- **Target repo path** — absolute path to the git repository to configure; inferred from current working directory if not stated, ask if ambiguous
+- **Task type** — install/configure, update hook, tune allowlist, debug finding; inferred from the user's request
+
+## Constraints
+
+- Always state what you are about to do before running `setup-gitleaks.sh` — the script modifies `.git/hooks/pre-commit` and seeds `.gitleaks.toml`
+- Never modify `.gitleaks.toml` if the user has not asked for allowlist changes — it is project-owned once seeded; treat it as user-controlled config
+- Never run `gitleaks git` or `gitleaks dir` across the full history without warning the user it may be slow on large repos
+- Redact any secret values that appear in gitleaks output before showing them to the user — show the rule ID, file, and line number only
+- When the installed gitleaks version is unknown, check it with `gitleaks version` before suggesting config syntax — v8.24.2 uses `[allowlist]`; v8.25.0+ uses `[[allowlists]]`
+- False positive suppression: prefer path-based allowlists in `.gitleaks.toml` over fingerprint-based entries in `.gitleaksignore` — fingerprints are line-number-sensitive and break on file edits
+
+
+
+
+
+## Process
+
+### Install and configure
+
+1. **Confirm target.** State: "I will run `scripts/setup-gitleaks.sh ` which will install gitleaks (if absent), seed `.gitleaks.toml` (first run only), and write the pre-commit hook. Proceed?" Wait for confirmation — this modifies the repo's git hook.
+
+2. **Run setup script.** Execute from the ai-development repo root:
+ ```
+ bash scripts/setup-gitleaks.sh
+ ```
+ The script is idempotent — it replaces the gitleaks block in the hook on every run without disturbing other hook content.
+
+3. **Verify installation.** Run `gitleaks version` to confirm the binary is available. Run `gitleaks git --staged --redact -v` in the target repo to confirm the hook would work on a staged commit (add a dummy change if needed to test).
+
+4. **Commit `.gitleaks.toml`.** Remind the user that `.gitleaks.toml` belongs in version control so all contributors share the same allowlist rules.
+
+### Update hook
+
+Re-run `bash scripts/setup-gitleaks.sh ` from the ai-development repo root. The managed block (delimited by `# managed by setup-gitleaks.sh` / `# end gitleaks` markers) is always replaced with the current version. Non-gitleaks hook content is preserved.
+
+### Tune allowlist / resolve false positives
+
+1. **Identify the false positive.** Run `gitleaks dir --log-level debug ` to see which rule fired and which allowlist entries (if any) are already active.
+
+2. **Check the gitleaks version.** Run `gitleaks version`. Use `[allowlist]` syntax for v8.24.2; use `[[allowlists]]` syntax for v8.25.0+. Using the wrong syntax silently produces no errors but the allowlist does nothing — this is the most common configuration trap.
+
+3. **Choose suppression strategy.** Read `.gitleaks.toml` first. See `references/allowlist-patterns.md` for syntax examples and when to use each approach:
+ - Path regex in `[allowlist]` — for files that can never contain real secrets (research notes, terminal captures, test fixtures). Preferred.
+ - Stopwords in `[allowlist]` — for placeholder patterns like "example", "changeme".
+ - `disabledRules` in `[extend]` — to disable a noisy default rule entirely. Use only when the rule has no value for this repo.
+ - `.gitleaksignore` fingerprint — last resort; breaks when the file is edited because line numbers shift.
+
+4. **Edit `.gitleaks.toml`.** Add the minimal allowlist entry needed. Do not suppress more than the identified false positive.
+
+5. **Verify.** Re-run `gitleaks dir -v ` or `gitleaks git -v` to confirm the false positive is suppressed and no real findings are hidden.
+
+### Scan modes
+
+| Mode | Command | When to use |
+|---|---|---|
+| Staged changes (pre-commit) | `gitleaks git --staged --redact -v` | What the hook runs |
+| Full commit history | `gitleaks git -v` | Audit existing repo history |
+| Working directory files | `gitleaks dir -v ` | Scan uncommitted files |
+| Debug allowlists | `gitleaks dir --log-level debug ` | See which files are skipped and which allowlists fire |
+
+### Resolve a real finding
+
+1. Do not redact or show the secret value. Reference the rule ID, file, and line number only.
+2. The secret is compromised the moment it was committed — rotate it immediately, regardless of whether the commit is reachable from the public remote.
+3. Remove the secret from history using `git filter-repo` (not `git filter-branch`). This is a history-rewrite — confirm with the user before running. Force-push to all remotes after rewriting.
+4. Add the file path to the `.gitleaks.toml` allowlist only if the file is known to be a false-positive source going forward (e.g. a test fixture). Do not add an allowlist entry to suppress a real finding that has been removed.
+
+## Output format
+
+No structured output file. The skill produces:
+- Modified `.git/hooks/pre-commit` in the target repo (via the setup script)
+- Modified `.gitleaks.toml` in the target repo (allowlist changes only, when requested)
+- Terminal confirmation of what was changed and what to do next
+
+
+
+
+
+## Failure handling
+
+- `setup-gitleaks.sh` not found — stop; instruct the user to run from the ai-development repo root at `/root/ai-development/`
+- Target path is not a git repository — report the error from the script and ask the user to confirm the correct path
+- `gitleaks` binary not installed and download fails — report the curl/network error; direct the user to manual install at `https://github.com/gitleaks/gitleaks/releases`
+- Wrong TOML syntax for installed version — detect via `gitleaks version`, show the correct syntax for that version, do not guess
+
+## Self-check
+
+- [ ] Target repo confirmed before running the setup script
+- [ ] `gitleaks version` checked before writing any `.gitleaks.toml` allowlist syntax
+- [ ] Secret values in scan output redacted before displaying to the user
+- [ ] `.gitleaks.toml` edits are minimal — only the identified false positive suppressed
+- [ ] After any allowlist change: re-ran scan to verify suppression works and no real findings are hidden
+- [ ] For real findings: rotation step stated before history rewrite, user confirmed history rewrite before running `git filter-repo`
+
+
diff --git a/.agents/skills/gitleaks/references/allowlist-patterns.md b/.agents/skills/gitleaks/references/allowlist-patterns.md
new file mode 100644
index 0000000..c5e17e6
--- /dev/null
+++ b/.agents/skills/gitleaks/references/allowlist-patterns.md
@@ -0,0 +1,83 @@
+# Gitleaks allowlist patterns
+
+## Version syntax
+
+| Version | Allowlist syntax |
+|---|---|
+| v8.24.2 and earlier | `[allowlist]` (singular table) |
+| v8.25.0 and later | `[[allowlists]]` (array of tables) |
+
+**Critical**: using the wrong syntax produces no error but the allowlist silently does nothing. Always check `gitleaks version` first.
+
+## v8.24.2 syntax (this repo uses 8.24.2)
+
+### Suppress by path regex
+
+Use for files that can never contain real secrets (research notes, terminal captures, test fixtures, generated docs).
+
+```toml
+[allowlist]
+description = "research notes and terminal captures"
+paths = [
+ '''docs/research/.*''',
+ '''tests/fixtures/.*''',
+]
+```
+
+### Suppress by stopword
+
+Use for placeholder values that match secret patterns but are clearly not real.
+
+```toml
+[allowlist]
+description = "placeholder values"
+stopwords = ["example", "placeholder", "changeme", "your-api-key-here"]
+```
+
+### Disable a default rule entirely
+
+Use only when a rule has no value for this repo and produces pervasive false positives.
+
+```toml
+[extend]
+useDefault = true
+disabledRules = ["generic-api-key"]
+```
+
+## v8.25.0+ syntax (for reference)
+
+```toml
+[[allowlists]]
+description = "research notes"
+paths = ['''docs/research/.*''']
+
+[[allowlists]]
+description = "placeholder values"
+stopwords = ["example", "placeholder"]
+```
+
+## .gitleaksignore (fingerprint-based — last resort)
+
+```
+# Format: :
+# Generated by: gitleaks git -v --report-format json | jq -r '.[] | "\(.Fingerprint):\(.StartLine)"'
+abc123def456:42
+```
+
+Avoid this approach: fingerprints embed line numbers. Any edit to the file shifts line numbers and invalidates the entry, re-surfacing the false positive.
+
+## Verification after any change
+
+```bash
+# Scan current files
+gitleaks dir -v .
+
+# Scan with debug output to see which allowlists fired
+gitleaks dir --log-level debug .
+
+# Scan commit history
+gitleaks git -v
+
+# Scan only staged changes (what the pre-commit hook runs)
+gitleaks git --staged --redact -v
+```
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..f740e1d
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,27 @@
+title = "gitleaks config"
+
+[extend]
+# Extends the default ruleset built into gitleaks.
+# Remove useDefault and define [[rules]] from scratch if you want full control.
+useDefault = true
+
+# Rules to disable from the default set — uncomment and add IDs for known false positives.
+# Run `gitleaks git -v` on your repo first to discover which rules fire.
+# disabledRules = ["generic-api-key"]
+
+# Project-specific allowlists — entries here apply to all rules.
+# Add fingerprints from .gitleaksignore, or path/regex patterns to suppress noise.
+#
+# Example: ignore test fixtures
+# [[allowlists]]
+# description = "test fixtures"
+# paths = ['''tests/fixtures/.*''']
+#
+# Example: ignore a known false-positive secret value
+# [[allowlists]]
+# description = "placeholder values used in docs"
+# stopwords = ["example", "placeholder", "changeme"]
+
+[allowlist]
+description = "research session notes — no secrets, high-entropy text from terminal captures"
+paths = ['''docs/research/.*''']
diff --git a/docs/spec/overview.md b/docs/spec/overview.md
index 8e7ce08..6e6a754 100644
--- a/docs/spec/overview.md
+++ b/docs/spec/overview.md
@@ -2,7 +2,7 @@
Current deployed state of this repo — what you get if you run `install.sh` today. Updated at the close of each chunk and in the same PR as any behavior change.
-*Last updated: 2026-05-18 (issue 0018 phase 1 refactor)*
+*Last updated: 2026-06-20 (gitleaks tooling + skill)*
## What is deployed
@@ -14,7 +14,7 @@ Current deployed state of this repo — what you get if you run `install.sh` tod
- `write-skill` — authors new SKILL.md files and converts placeholders to canonical format. Hand-written (bootstrap — cannot author itself before it exists). Eval at `.agents/evals/factory/write-skill/eval.yaml`. Invokes `write-eval` as part of its own process.
- `write-docs` — produces technical documentation derived from code and spec; never invents behaviour. **First factory-authored skill** (SKILL.md produced via `write-skill`, eval via `write-eval`). Eval at `.agents/evals/implement/write-docs/eval.yaml`. Sources: anthropics/skills `doc-coauthoring`, mattpocock/skills `write-a-skill`, bmad-code-org/BMAD-METHOD `bmad-advanced-elicitation`.
-Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`.
+Current skills: `caveman`, `diagnose`, `gitleaks`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`.
**Chunk 3 target:** 42 skills across 9 categories. PRD: `docs/prd/chunk-3-skills-library.md`. Canonical build reference: `docs/research/ai-coding-factory/ai-coding-factory-skills-index.md` (delete once all skills exist). Skills stored flat (`skill-name/SKILL.md`) per ADR-0009; category in `metadata.category` frontmatter. Categories: design, factory, implement, test, review, deploy, operate, cross-cutting, iac (2 skills only — docker-compose + iac-security-review). Role skills (6) deferred to Chunk 5. Gitea skills moved to `providers/gitea/` provider adapter.
@@ -42,11 +42,14 @@ Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-c
- `init-project.sh` — bootstraps a new project (Chunk 6)
- Copilot provider adapter (Chunk 7)
- Formal CI/pre-commit enforcement of governance rules (Chunk 6)
+- `setup-gitleaks.sh` not yet wired into `init-project.sh` (Chunk 6) — run manually against new repos
For chunk planning and open questions, see `docs/ROADMAP.md`.
## Recent changes
+- 2026-06-20 — Gitleaks secret scanning added. `scripts/setup-gitleaks.sh` installs gitleaks v8.24.2, seeds `.gitleaks.toml` (first run only — project-owned after that), and writes a managed pre-commit hook block that is replaced on re-run. `scripts/gitleaks.toml` is the base config template extending gitleaks defaults. `tests/test-setup-gitleaks.sh` covers 6 behaviors (reject non-git dir, config deploy, hook create, append, stale-block replace, idempotency). `.gitleaks.toml` in repo root adds path allowlist for `docs/research/` (high-entropy terminal captures). `gitleaks` skill added (`cross-cutting`) covering full lifecycle: install, update, tune allowlist, scan modes, resolve real findings. Key lesson: v8.24.2 uses `[allowlist]` (singular); v8.25.0+ uses `[[allowlists]]` — wrong syntax silently does nothing.
+
- 2026-05-18 — Issue 0018 phase 1 refactor complete: `write-skill` redesigned from scratch. New files added to skill directory: `SKILL-TEMPLATE.md` (authoritative 6-section template with XML blocks, human-usable), `META-TEMPLATE.md` (provenance schema with inline-commented YAML), `CATEGORIES.md` (self-contained category table), `META.md` (write-skill's own provenance). SKILL.md rewritten: 6 sections replacing 8 (Role and When/When not dropped — not in agentskills.io spec); frontmatter reduced to 3 fields (`name`, `description`, `metadata.category`); provenance fields (`version`, `updated`, `when`, `source`, `references`) moved to META.md (progressive disclosure — not loaded at startup). `docs/notes/skill-implementation-workflow.md` updated to reference SKILL-TEMPLATE.md as the authoritative template.
- 2026-05-17 — Issue 0018 phase 2 complete: `write-docs` skill written and deployed. First skill produced end-to-end by the factory (SKILL.md via `write-skill`, eval via `write-eval`). Category: implement. Key decisions: file-approval gate before reading (user names files or approves proposals); gap check before drafting (user fills what code doesn't explain); stage skipping allowed with logged reason; full revised section shown before confirmation gate; surgical edits only with per-round delta summary; Reader Testing via scoped sub-agent (doc + questions only, no source files); summary/overview sections written last. Sources: anthropics/skills doc-coauthoring (Reader Testing stage, surgical-edit constraint), mattpocock/skills write-a-skill (trigger pattern), bmad-code-org/BMAD-METHOD bmad-advanced-elicitation (confirmation gate). Open follow-up: documentation convention (file/folder/content structure, global vs repo-specific) — not yet defined.
diff --git a/scripts/gitleaks.toml b/scripts/gitleaks.toml
new file mode 100644
index 0000000..97b8fa8
--- /dev/null
+++ b/scripts/gitleaks.toml
@@ -0,0 +1,24 @@
+title = "gitleaks config"
+
+[extend]
+# Extends the default ruleset built into gitleaks.
+# Remove useDefault and define [[rules]] from scratch if you want full control.
+useDefault = true
+
+# Rules to disable from the default set — uncomment and add IDs for known false positives.
+# Run `gitleaks git -v` on your repo first to discover which rules fire.
+# disabledRules = ["generic-api-key"]
+
+# Global allowlist — applies to all rules.
+# Note: uses [allowlist] (v8 syntax). v8.25.0+ uses [[allowlists]] (array of tables).
+# Add path regexes or stopwords to suppress known false positives.
+#
+# Example: ignore test fixtures
+# [allowlist]
+# description = "test fixtures"
+# paths = ['''tests/fixtures/.*''']
+#
+# Example: ignore a known false-positive secret value
+# [allowlist]
+# description = "placeholder values in docs"
+# stopwords = ["example", "placeholder", "changeme"]
diff --git a/scripts/setup-gitleaks.sh b/scripts/setup-gitleaks.sh
new file mode 100755
index 0000000..ea322c5
--- /dev/null
+++ b/scripts/setup-gitleaks.sh
@@ -0,0 +1,124 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Sets up gitleaks as a git pre-commit hook in a target repository.
+# Usage: setup-gitleaks.sh [TARGET_REPO]
+# TARGET_REPO — path to the git repo to configure (default: current directory)
+# Idempotent: safe to re-run; always replaces the hook block with the current version.
+
+GITLEAKS_VERSION="8.24.2"
+GITLEAKS_INSTALL_DIR="${GITLEAKS_INSTALL_DIR:-/usr/local/bin}"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TARGET="${1:-$(pwd)}"
+HOOK_FILE="$TARGET/.git/hooks/pre-commit"
+CONFIG_SRC="$SCRIPT_DIR/gitleaks.toml"
+CONFIG_DEST="$TARGET/.gitleaks.toml"
+MARKER="# managed by setup-gitleaks.sh"
+END_MARKER="# end gitleaks"
+
+# --- Install gitleaks if not present ---
+
+install_gitleaks() {
+ local os arch tarball url tmp_dir
+
+ case "$(uname -s)" in
+ Linux) os="linux" ;;
+ Darwin) os="darwin" ;;
+ *)
+ echo "Error: unsupported OS '$(uname -s)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2
+ exit 1
+ ;;
+ esac
+
+ case "$(uname -m)" in
+ x86_64) arch="x64" ;;
+ aarch64 | arm64) arch="arm64" ;;
+ *)
+ echo "Error: unsupported architecture '$(uname -m)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2
+ exit 1
+ ;;
+ esac
+
+ tarball="gitleaks_${GITLEAKS_VERSION}_${os}_${arch}.tar.gz"
+ url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}"
+ tmp_dir="$(mktemp -d)"
+ trap 'rm -rf "$tmp_dir"' RETURN
+
+ echo "Installing gitleaks v${GITLEAKS_VERSION}..."
+ curl -fsSL "$url" -o "$tmp_dir/$tarball"
+ tar -xzf "$tmp_dir/$tarball" -C "$tmp_dir" gitleaks
+ install -m 755 "$tmp_dir/gitleaks" "$GITLEAKS_INSTALL_DIR/gitleaks"
+ echo "Installed: $GITLEAKS_INSTALL_DIR/gitleaks"
+}
+
+if ! command -v gitleaks &>/dev/null; then
+ install_gitleaks
+fi
+
+# --- Validate ---
+
+if [ ! -d "$TARGET/.git" ]; then
+ echo "Error: $TARGET is not a git repository" >&2
+ exit 1
+fi
+
+if [ ! -f "$CONFIG_SRC" ]; then
+ echo "Error: config template not found at $CONFIG_SRC" >&2
+ exit 1
+fi
+
+# --- Deploy config ---
+
+if [ -f "$CONFIG_DEST" ]; then
+ echo "Skipped: $CONFIG_DEST already exists — edit it directly to customise rules."
+else
+ cp "$CONFIG_SRC" "$CONFIG_DEST"
+ echo "Wrote: $CONFIG_DEST"
+ echo " Commit this file — it belongs in version control."
+fi
+
+# --- Deploy hook ---
+
+hook_block() {
+ cat </dev/null; then
+ gitleaks git --staged --redact -v
+else
+ echo "Warning: gitleaks not installed — secret scan skipped (https://github.com/gitleaks/gitleaks/releases)" >&2
+fi
+$END_MARKER
+BLOCK
+}
+
+write_hook() {
+ local hook_file="$1"
+
+ if grep -qF "$MARKER" "$hook_file"; then
+ # Remove old block (start marker through end marker inclusive) then append current version
+ awk -v start="$MARKER" -v end="$END_MARKER" '
+ $0 == start { skip=1; next }
+ skip && $0 == end { skip=0; next }
+ !skip { print }
+ ' "$hook_file" > "${hook_file}.tmp" && mv "${hook_file}.tmp" "$hook_file"
+ hook_block >> "$hook_file"
+ echo "Updated: $hook_file (gitleaks block replaced)"
+ else
+ hook_block >> "$hook_file"
+ echo "Updated: $hook_file (gitleaks block appended to existing hook)"
+ fi
+}
+
+if [ -f "$HOOK_FILE" ]; then
+ write_hook "$HOOK_FILE"
+else
+ { echo '#!/usr/bin/env bash'; echo 'set -euo pipefail'; hook_block; } > "$HOOK_FILE"
+ chmod +x "$HOOK_FILE"
+ echo "Created: $HOOK_FILE"
+fi
+
+echo ""
+echo "Done. Staged secrets will be scanned on every commit in $TARGET."
+echo "To skip on a single commit: SKIP=gitleaks git commit ..."
diff --git a/tests/test-setup-gitleaks.sh b/tests/test-setup-gitleaks.sh
new file mode 100755
index 0000000..57c81b2
--- /dev/null
+++ b/tests/test-setup-gitleaks.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+SCRIPT="$REPO_ROOT/scripts/setup-gitleaks.sh"
+PASS=0
+FAIL=0
+
+pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
+fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
+
+# Fake gitleaks binary — prevents the install step from running during tests
+FAKE_BIN="$(mktemp -d)"
+trap 'rm -rf "$FAKE_BIN"' EXIT
+printf '#!/bin/sh\necho "gitleaks fake"\n' > "$FAKE_BIN/gitleaks"
+chmod +x "$FAKE_BIN/gitleaks"
+export PATH="$FAKE_BIN:$PATH"
+
+# Helper: create an isolated git repo in a temp dir
+make_repo() {
+ local dir
+ dir="$(mktemp -d)"
+ git -C "$dir" init -q
+ echo "$dir"
+}
+
+# Helper: run setup script against a target repo; capture output; always return exit code
+run_setup() {
+ local target="$1"
+ bash "$SCRIPT" "$target" 2>&1
+}
+
+# --- 1. Rejects non-git directory ---
+echo ""
+echo "--- rejects non-git directory ---"
+NON_GIT="$(mktemp -d)"
+trap 'rm -rf "$NON_GIT"' EXIT
+if bash "$SCRIPT" "$NON_GIT" >/dev/null 2>&1; then
+ fail "exited 0 for non-git directory — expected exit 1"
+else
+ pass "exits non-zero for non-git directory"
+fi
+
+# --- 2. Config deployed ---
+echo ""
+echo "--- config deployed to target repo ---"
+REPO="$(make_repo)"
+trap 'rm -rf "$REPO"' EXIT
+run_setup "$REPO" > /dev/null
+if [ -f "$REPO/.gitleaks.toml" ]; then
+ pass ".gitleaks.toml created in target repo"
+else
+ fail ".gitleaks.toml missing from target repo"
+fi
+if diff -q "$REPO_ROOT/scripts/gitleaks.toml" "$REPO/.gitleaks.toml" > /dev/null 2>&1; then
+ pass ".gitleaks.toml matches the template"
+else
+ fail ".gitleaks.toml content differs from template"
+fi
+
+# --- 3. Hook created from scratch ---
+echo ""
+echo "--- hook created when none exists ---"
+REPO2="$(make_repo)"
+trap 'rm -rf "$REPO2"' EXIT
+run_setup "$REPO2" > /dev/null
+HOOK="$REPO2/.git/hooks/pre-commit"
+if [ -f "$HOOK" ]; then
+ pass "pre-commit hook created"
+else
+ fail "pre-commit hook not created"
+fi
+if [ -x "$HOOK" ]; then
+ pass "pre-commit hook is executable"
+else
+ fail "pre-commit hook is not executable"
+fi
+if head -1 "$HOOK" | grep -q "^#!"; then
+ pass "pre-commit hook has a shebang"
+else
+ fail "pre-commit hook missing shebang"
+fi
+if grep -q "gitleaks git --staged" "$HOOK"; then
+ pass "pre-commit hook contains gitleaks command"
+else
+ fail "pre-commit hook missing gitleaks command"
+fi
+if grep -q "# managed by setup-gitleaks.sh" "$HOOK"; then
+ pass "pre-commit hook contains idempotency marker"
+else
+ fail "pre-commit hook missing idempotency marker"
+fi
+
+# --- 4. Appends to existing hook; existing content retained ---
+echo ""
+echo "--- appends to existing hook; prior content retained ---"
+REPO3="$(make_repo)"
+trap 'rm -rf "$REPO3"' EXIT
+HOOK3="$REPO3/.git/hooks/pre-commit"
+printf '#!/usr/bin/env bash\nnpm test\n' > "$HOOK3"
+chmod +x "$HOOK3"
+run_setup "$REPO3" > /dev/null
+if grep -q "npm test" "$HOOK3"; then
+ pass "existing hook content retained after append"
+else
+ fail "existing hook content lost after append"
+fi
+if grep -q "gitleaks git --staged" "$HOOK3"; then
+ pass "gitleaks block appended to existing hook"
+else
+ fail "gitleaks block missing after append"
+fi
+
+# --- 5. Second run replaces stale block; existing content still retained ---
+echo ""
+echo "--- second run replaces stale block; existing content still retained ---"
+# Corrupt the gitleaks block to simulate stale content from an older version
+sed -i 's/gitleaks git --staged/gitleaks protect --staged/' "$HOOK3"
+run_setup "$REPO3" > /dev/null
+if grep -q "npm test" "$HOOK3"; then
+ pass "existing content retained after block replacement"
+else
+ fail "existing content lost after block replacement"
+fi
+marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")"
+if [ "$marker_count" -eq 1 ]; then
+ pass "gitleaks block appears exactly once after second run"
+else
+ fail "gitleaks block duplicated — found $marker_count occurrences of marker"
+fi
+if grep -q "gitleaks git --staged" "$HOOK3"; then
+ pass "stale gitleaks command replaced with current command"
+else
+ fail "stale gitleaks command not replaced — block was skipped, not updated"
+fi
+
+# --- 6. Third run still idempotent ---
+echo ""
+echo "--- repeated runs stay idempotent ---"
+run_setup "$REPO3" > /dev/null
+run_setup "$REPO3" > /dev/null
+marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")"
+if [ "$marker_count" -eq 1 ]; then
+ pass "gitleaks block still appears exactly once after four total runs"
+else
+ fail "gitleaks block duplicated — found $marker_count occurrences after four runs"
+fi
+
+echo ""
+echo "Results: $PASS passed, $FAIL failed"
+[[ $FAIL -eq 0 ]]