Initialize Bulwark infrastructure in a project: language-aware Justfile (8 langs), bun + eval-framework toolchain, logs/ subdirectories, and optional hooks.
Installs into .claude/skills of the current project.
Are you the author of Bulwark Scaffold?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/qball-inc-bulwark-scaffold)
---
name: bulwark-scaffold
description: Initialize Bulwark infrastructure in a project: language-aware Justfile (8 langs), bun + eval-framework toolchain, logs/ subdirectories, and optional hooks.
when_to_use: Use when the user asks to set up Bulwark in a project, scaffold a Justfile, configure Bulwark hooks, or initialize the eval framework. Do NOT auto-invoke during normal development — this writes files, runs installers, and modifies .gitignore. Also invoked by the Bulwark init skill when scaffolding is selected.
argument-hint: "[--force] [--no-hooks] [--dry-run] [--lang=<node|python|rust|go|kotlin|swift|shell|generic>]"
user-invocable: true
version: 1.0.1
author: "Ashay Kubal @ Qball Inc."
---
# Bulwark Scaffold
Initialize Bulwark infrastructure in a project by generating Justfile templates, creating the logs directory structure, and optionally configuring hooks.
---
## Mandatory Execution Checklist (BINDING)
**Every item below is mandatory. No deviations. No substitutions. No skipping. Skipping items violates SC1-SC3 (Skill Compliance Rules in Rules.md).**
You are the orchestrator. Follow every item in order. Do NOT return to the user until all applicable items are checked.
- [ ] **Step 1 — Parse arguments**: `--force`, `--no-hooks`, `--dry-run`, `--lang=<...>` extracted from `$ARGUMENTS`
- [ ] **Step 2 — Detect project language**: If `--lang` not supplied, project files inspected (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, etc.); defaults to `generic` when no signal found
- [ ] **Step 3 — `just` runtime check**: `command -v just` runs; if missing, follow Step 3 install path
- [ ] **Step 3.5a — `bun` runtime check**: `bash <resolved-installer-path> --verify` invoked (NOT `command -v bun` — that bypasses version check); follow Step 3.5 install path if exit non-zero
- [ ] **Step 3.5b — Installer copy**: `scripts/install-bun.sh` copied to `${CLAUDE_PROJECT_DIR}/scripts/` so Justfile recipes can reference it project-relatively
- [ ] **Step 3.6 — Existing Justfile check**: If a Justfile is already present, abort unless `--force` (with backup created)
- [ ] **Step 4 — Dry-run check**: If `--dry-run`, preview the planned writes and exit without modifying disk
- [ ] **Step 5 — Justfile generation**: Template emitted from `lib/templates/justfile-<lang>.just`; bun + eval recipes (`install-bun`, `verify-bun`, `eval-skill`, `eval-grade`, `eval`) confirmed present in output
- [ ] **Step 6 — `logs/` subdirectories**: `diagnostics/`, `validations/`, `debug-reports/` created
- [ ] **Step 7 — `.gitignore`**: Bulwark log patterns appended idempotently
- [ ] **Step 8 — Hooks**: If plugin-level hooks active, SKIP `.claude/settings.json` hook injection (anti-duplication)
- [ ] **Step 9 — Scaffold log**: `logs/scaffold-{ts}.yaml` written with top-level `reviewed_files: [...]` (Stop-hook contract)
- [ ] **Step 10 — Report results**: User-facing summary emitted listing files written, installer outcomes, and any skipped steps
---
## Usage
```
/bulwark-scaffold [options]
```
**Options:**
- `--force` - Overwrite existing Justfile (creates backup)
- `--no-hooks` - Skip hook configuration (hooks are generated by default)
- `--dry-run` - Show what would be generated without writing files
- `--lang=<node|python|rust|go|kotlin|swift|shell|generic>` - Override language detection
**Examples:**
- `/bulwark-scaffold` - Full scaffold with Justfile + logs/ + hooks
- `/bulwark-scaffold --force` - Overwrite existing Justfile
- `/bulwark-scaffold --no-hooks` - Skip hook configuration
- `/bulwark-scaffold --dry-run` - Preview changes
---
## Execution Steps
### Step 1: Parse Arguments
Extract options from `$ARGUMENTS`:
- `--force` → FORCE_OVERWRITE=true
- `--no-hooks` → SKIP_HOOKS=true
- `--dry-run` → DRY_RUN=true
- `--lang=X` → LANG_OVERRIDE=X
### Step 2: Detect Project Language
If LANG_OVERRIDE is set, use that. Otherwise, search from current directory for manifest files in this order (first match wins):
1. `package.json` → **node**
2. `pyproject.toml` OR `requirements.txt` OR `setup.py` → **python**
3. `Cargo.toml` → **rust**
4. `go.mod` → **go**
5. `build.gradle` OR `build.gradle.kts` OR any `*.gradle.kts` file → **kotlin**
6. `Package.swift` → **swift**
7. No manifest above AND at least one `*.sh` file at project root or in `scripts/` → **shell**
8. None of the above → **fallthrough** (do NOT silently default to generic; see Step 5)
Store result in DETECTED_LANG variable. If fallthrough, DETECTED_LANG=`fallthrough` (a marker value, not a template name).
### Step 3: Verify `just` runtime
**Pre-flight: Verify `just` is installed.**
```
Run: command -v just
IF just is NOT found:
Delegate the install attempt to scripts/install-just.sh — it detects the
platform (macOS/WSL/Debian/RHEL/Windows) and uses the right package
manager (brew/cargo/apt/dnf/winget/scoop) with cargo as primary for
Linux. Does NOT use curl|bash.
Locate the installer:
- Plugin install: ${CLAUDE_PLUGIN_ROOT}/scripts/install-just.sh
- Local dev: ${CLAUDE_PROJECT_DIR}/scripts/install-just.sh
Ask user: "just is required for the scaffolded Justfile. Install it now via scripts/install-just.sh (recommended) or skip Justfile generation?"
- If install: Run `bash <installer-path>`
- Exit 0: just installed → continue
- Exit non-zero: installer already printed actionable per-platform
instructions → set SKIP_JUSTFILE=true (do not re-print)
- If skip: Set SKIP_JUSTFILE=true
If the installer script cannot be located (rare — repo corruption):
Print: "Could not find scripts/install-just.sh. Install just manually:"
Print: " macOS: brew install just"
Print: " Linux: cargo install just (requires Rust: https://rustup.rs)"
Print: " Windows: winget install Casey.Just"
Print: " All: https://just.systems/man/en/chapter_4.html"
Set SKIP_JUSTFILE=true
```
### Step 3.5: Verify bun runtime (eval framework + generated TS scripts)
**Pre-flight: Verify `bun` is installed (required for create-skill eval framework + generated archetype scripts).**
```
1. Locate the source installer (prefer plugin install path; fall back to local-dev):
- Plugin install: ${CLAUDE_PLUGIN_ROOT}/scripts/install-bun.sh
- Local dev: ${CLAUDE_PROJECT_DIR}/scripts/install-bun.sh
IF neither path exists (rare — repo corruption):
Print: "Could not find scripts/install-bun.sh. Install bun manually:"
Print: " macOS: brew install oven-sh/bun/bun"
Print: " Linux: curl -fsSL https://bun.sh/install | bash"
Print: " Windows: powershell -c \"irm bun.sh/install.ps1 | iex\""
Print: " Docs: https://bun.sh/docs/installation"
Set SKIP_BUN_FEATURES=true
Skip the rest of Step 3.5
2. Copy the installer into the project so the Justfile recipes can reference
it project-relatively (the templates use `./scripts/install-bun.sh`):
mkdir -p ${CLAUDE_PROJECT_DIR}/scripts
cp <resolved-installer-path> ${CLAUDE_PROJECT_DIR}/scripts/install-bun.sh
chmod +x ${CLAUDE_PROJECT_DIR}/scripts/install-bun.sh
(Idempotent — overwriting any prior copy ensures the project mirrors the
currently-installed plugin version.)
3. Run the verify probe with the project-local path (NOT `command -v bun`,
which would bypass version checks performed by the installer):
bash ${CLAUDE_PROJECT_DIR}/scripts/install-bun.sh --verify
4. IF bun is NOT found OR version < 1.0 (verify exit non-zero):
Ask user: "bun is required for the create-skill eval framework and generated TS scripts. Install it now via scripts/install-bun.sh (recommended) or skip eval-framework features?"
- If install: Run `bash ${CLAUDE_PROJECT_DIR}/scripts/install-bun.sh`
- Exit 0: bun installed → continue
- Exit non-zero: installer already printed actionable per-platform
instructions → set SKIP_BUN_FEATURES=true (do not re-print)
- If skip: Set SKIP_BUN_FEATURES=true; user can install later via `just install-bun`
5. IF bun is found (verify exit 0):
Continue (no install needed; idempotent verify).
```
When `bun` is verified or installed AND the templates have been emitted, the recipes `just install-bun`, `just verify-bun`, `just eval-skill`, `just eval-grade`, and `just eval` are runnable. The first two delegate to the project-local `scripts/install-bun.sh` (copied in step 2); the eval recipes run TS scripts under `skills/create-skill/scripts/` via bun. When `SKIP_BUN_FEATURES=true`, the scaffold continues but the user is informed those features will fail until `just install-bun` runs successfully.
The `bun` check is parallel to the `just` check (Step 3) — both are runtime installers; both follow the same locate-then-execute pattern; both have actionable manual fallbacks.
### Step 3.6: Check for existing Justfile
```
IF Justfile exists AND NOT FORCE_OVERWRITE:
Print: "Justfile already exists. Use --force to overwrite (creates backup)."
Set SKIP_JUSTFILE=true
ELSE IF Justfile exists AND FORCE_OVERWRITE:
Create backup: Justfile.backup-{YYYYMMDD-HHMMSS}
Set SKIP_JUSTFILE=false
ELSE:
Set SKIP_JUSTFILE=false
```
### Step 4: Dry Run Check
If DRY_RUN is true, display preview and exit:
```
## Scaffold Preview (dry-run)
**Language detected:** {DETECTED_LANG} ({manifest file or "no manifest"})
**Would create:**
- Justfile (from `lib/templates/justfile-{RESOLVED_TEMPLATE}.just` — substitute the actual filename, e.g., `justfile-go.just`, NOT the interpolation placeholder)
- logs/
- logs/diagnostics/
- logs/validations/
- logs/debug-reports/
{IF NOT SKIP_HOOKS}
- .claude/settings.json (hooks configuration)
- .claude/skills/governance-protocol/SKILL.md
- scripts/hooks/inject-protocol.sh
- scripts/hooks/enforce-quality.sh
- scripts/hooks/suggest-pipeline-stop.sh
{ENDIF}
**Would update:**
- .gitignore (add Bulwark patterns)
Run without --dry-run to apply changes.
```
Then STOP execution.
### Step 5: Generate Justfile
If NOT SKIP_JUSTFILE:
**Template selection:**
If DETECTED_LANG is one of `node|python|rust|go|kotlin|swift|shell`:
- TEMPLATE_NAME = `justfile-{DETECTED_LANG}.just`
If DETECTED_LANG is `fallthrough` (no manifest detected, no --lang override):
- Do NOT silently copy `justfile-generic.just`. The generic template is now
fail-loudly (every recipe exits 1 with an "unconfigured" error), so handing
it to the user without explicit consent produces a broken-looking Justfile.
- Prompt the user:
```
No language manifest detected and no --lang override provided.
Choose a language template:
1) node - eslint + tsc + vitest recipes
2) python - ruff + mypy + pytest recipes
3) rust - cargo fmt + clippy + test recipes
4) go - go vet + golangci-lint + gofmt recipes
5) kotlin - ktlint + detekt recipes
6) swift - swiftlint + swift-format recipes
7) shell - shellcheck + shfmt recipes
8) generic - placeholder Justfile that FAILS LOUDLY on every recipe
(use only if you will hand-edit it immediately)
9) cancel - skip Justfile generation
Select [1-9]:
```
- Record the user's choice in DETECTED_LANG. If option 9, set SKIP_JUSTFILE=true.
- If option 8 (generic), print a warning before copying:
```
WARNING: The generic Justfile fails every recipe with exit 1 by design.
You must either (a) pick a language and re-run scaffold, or
(b) replace the `_not_configured` recipe calls with real tool invocations
before running `just <recipe>`.
```
- Then TEMPLATE_NAME = `justfile-{DETECTED_LANG}.just` (with the user's choice).
**Copy template:**
1. Locate template: `${CLAUDE_PLUGIN_ROOT}/lib/templates/{TEMPLATE_NAME}`
- For local development: `${CLAUDE_PROJECT_DIR}/lib/templates/{TEMPLATE_NAME}`
2. Copy template content to `Justfile` in project root
### Step 6: Create logs/ Directory Structure
```bash
mkdir -p logs/diagnostics logs/validations logs/debug-reports
touch logs/.gitkeep logs/diagnostics/.gitkeep logs/validations/.gitkeep logs/debug-reports/.gitkeep
```
### Step 7: Update .gitignore
Check if `.gitignore` exists and contains `# Bulwark logs` marker:
- If marker exists: Skip (already configured)
- If marker does not exist: Append the following patterns
**Patterns to add:**
```gitignore
# Bulwark logs
logs/*.yaml
logs/*.log
logs/diagnostics/*.yaml
logs/validations/*.yaml
logs/debug-reports/*.yaml
!logs/.gitkeep
!logs/*/.gitkeep
```
### Step 8: Generate Hook Configuration (Default)
UNLESS SKIP_HOOKS is true:
**Pre-flight: Check for existing Bulwark hooks to prevent duplication.**
Plugin hooks (`hooks/hooks.json`) and settings hooks (`.claude/settings.json`) both fire at runtime. If hooks already exist in any location, adding them again causes double execution.
**Check these locations for existing Bulwark hooks** (search for `enforce-quality` or `inject-protocol`):
| Location | Scope | Notes |
|----------|-------|-------|
| `./hooks/hooks.json` | Project plugin | Bulwark installed as project-level plugin |
| `~/.claude/plugins/*/hooks/hooks.json` | User plugin | Bulwark installed at user level via `/install` |
| `.claude/settings.json` | Project settings | Previously scaffolded |
| `.claude/settings.local.json` | Project local | User-specific project overrides |
| `~/.claude/settings.json` | User settings | Global user hooks |
| `~/.claude/settings.local.json` | User local | Global user overrides |
```
HOOKS_FOUND_IN = []
FOR each location above:
IF file exists AND contains "enforce-quality" OR "inject-protocol":
Append location to HOOKS_FOUND_IN
IF HOOKS_FOUND_IN is not empty:
Print: "Bulwark hooks already present in: {HOOKS_FOUND_IN}. Skipping hook generation to prevent duplication."
Set SKIP_HOOKS=true
```
**If no existing hooks found**, proceed with hook generation:
Check if `.claude/settings.json` exists:
- If exists: Merge hooks into existing configuration (preserve other settings)
- If not exists: Create new file
**Hook configuration to add:**
```json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/scripts/hooks/inject-protocol.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/scripts/hooks/enforce-quality.sh",
"timeout": 60
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/scripts/hooks/suggest-pipeline-stop.sh",
"timeout": 30
}
]
}
]
}
}
```
**Note:** SessionStart has no matcher, so it fires for all session events (start, resume, clear, compact). The `Stop` hook fires once at Claude turn end and emits a single consolidated pipeline-suggestion block per turn (mirrors the plugin's own `hooks/hooks.json`). Do NOT install the deprecated `suggest-pipeline.sh` — it emits per-edit blocks and causes the hook storm P10.1 fixes.
**Also copy required files** (only if hooks were generated above):
1. Copy `scripts/hooks/inject-protocol.sh` to `${CLAUDE_PROJECT_DIR}/scripts/hooks/`
2. Copy `scripts/hooks/enforce-quality.sh` to `${CLAUDE_PROJECT_DIR}/scripts/hooks/`
3. Copy `scripts/hooks/suggest-pipeline-stop.sh` to `${CLAUDE_PROJECT_DIR}/scripts/hooks/`
4. Copy `skills/governance-protocol/SKILL.md` to `${CLAUDE_PROJECT_DIR}/.claude/skills/governance-protocol/SKILL.md`
Create parent directories as needed (`mkdir -p`).
### Step 9: Write Scaffold Log
Write to `logs/scaffold-{YYYYMMDD-HHMMSS}.yaml`:
```yaml
# Top-level — required for Stop-hook per-file pipeline-recursion suppression.
# List every script/.sh file generated or copied during scaffolding (e.g. the
# project copy of install-bun.sh, any hook scripts copied to scripts/hooks/).
# Paths relative to ${CLAUDE_PROJECT_DIR}. Empty list `[]` if no script files
# were touched. Missing field disables suppression for this log (strict mode).
reviewed_files:
- scripts/install-bun.sh
- scripts/hooks/enforce-quality.sh
metadata:
timestamp: {ISO-8601}
action: scaffold
invocation: "/bulwark-scaffold {args}"
detection:
# language must be one of: node | python | rust | go | kotlin | swift | shell | generic | fallthrough
language: {DETECTED_LANG}
manifest: {manifest path or null}
override: {LANG_OVERRIDE or null}
user_selected: {true|false} # true if user picked a language at the fallthrough prompt
actions:
justfile:
action: created|skipped|overwritten
backup: {backup path or null}
# template filename resolves to one of:
# justfile-node.just | justfile-python.just | justfile-rust.just
# justfile-go.just | justfile-kotlin.just | justfile-swift.just
# justfile-shell.just | justfile-generic.just
template: lib/templates/justfile-{RESOLVED_TEMPLATE}.just
logs_directory:
created: true
subdirectories: [diagnostics, validations, debug-reports]
gitignore:
action: created|updated|skipped
patterns_added: 7
hooks:
action: created|merged|skipped
path: .claude/settings.json
skipped_reason: {reason if skipped}
summary: |
Scaffold complete for {DETECTED_LANG} project.
```
### Step 10: Report Results
Present summary to user:
```
## Scaffold Complete
**Language:** {DETECTED_LANG}
**Justfile:** {created|skipped|overwritten (backup: path)}
**logs/:** Created with subdirectories (diagnostics, validations, debug-reports)
**.gitignore:** {updated|created|unchanged}
**Hooks:** {created|merged|skipped (--no-hooks)}
**Governance:** {installed|skipped} - Protocol injected at session start
Run `just` to see available recipes:
- `just typecheck` - Run type checker
- `just lint` - Run linter
- `just build` - Build project
- `just test` - Run tests
- `just ci` - Run all quality checks
- `just fix` - Auto-fix issues
```
---
## Error Handling
| Scenario | Action |
|----------|--------|
| Cannot detect language | Prompt user to pick from the Step 5 menu. Do NOT silently default to generic — it produces a fail-loudly Justfile by design. |
| Invalid --lang value | Print: "Invalid --lang. Use one of: node, python, rust, go, kotlin, swift, shell, generic" and exit. |
| Template file missing | Print: "Template not found at {path}. Bulwark installation may be corrupted." |
| Cannot create logs/ | Print: "Cannot create logs/ directory. Check permissions." |
| .gitignore write fails | Print warning, continue with other operations |
| Justfile backup fails | Print error, abort Justfile generation |
---
## Diagnostic Output
Write diagnostic log to `logs/diagnostics/bulwark-scaffold-{timestamp}.yaml`:
```yaml
# Top-level — mirror the same list emitted in the scaffold report (Stop hook contract).
reviewed_files:
- scripts/install-bun.sh
- scripts/hooks/enforce-quality.sh
skill: bulwark-scaffold
timestamp: {ISO-8601}
invocation: "{full command}"
inputs:
force: {true|false}
no_hooks: {true|false}
dry_run: {true|false}
lang_override: {value or null}
detection:
language: {detected}
manifest_path: {path or null}
outputs:
justfile_created: {true|false}
logs_created: {true|false}
gitignore_updated: {true|false}
hooks_configured: {true|false}
errors: []
```