This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Vulcan helps security teams create STIG-ready security guidance documentation. Users author "Components" (STIGs in progress) that implement requirements from SRGs (Security Requirements Guides). Once reviewed by DISA/DoD, Components become published STIGs.
Key Concept: Component = "STIG in progress" - same structure, different lifecycle stage.
These rules override EVERYTHING else. No exceptions. No excuses.
-
WHEN THE USER IS FRUSTRATED, SLOW DOWN - Do NOT try to move faster. Do NOT bulk-operate without verifying each item. Do NOT fabricate explanations. VERIFY EVERYTHING. Say "I don't know" when you don't know. Moving fast when trust is broken destroys the relationship. The user WILL call you out. You WILL redo fucked up work. Get it right the first time by slowing down.
-
CORRECT CODE OVER SPEED - Never rush. Never cut corners. Take the time to do it right.
-
FIX ALL BUGS WHEN FOUND - Period. No "pre-existing" excuses. No "out of scope" dismissals. We own ALL the code in this repository. If you find a bug, you fix it. NOW.
-
BEST PRACTICES AND STANDARDS — ALWAYS, NO EXCEPTIONS - Always use DRY, best practice, maintainable, standards-compliant solutions. Research the proper way. Follow established patterns. No shortcuts. No quick fixes. No hacks. No workarounds. No "document what exists and card the real fix for later." If the code is wrong, FIX THE CODE. If the API is inconsistent, MAKE IT CONSISTENT. If there is a proper pattern, USE IT. Every single time.
-
NO HACKS, WORKAROUNDS, OR "JUST MAKING IT WORK" - If the solution feels like a hack, it IS a hack. Stop. Find the proper solution.
-
DO NOT GUESS - RESEARCH FIRST - Before trying solutions, RESEARCH the problem. Search GitHub issues, documentation, Stack Overflow. Find how others solved it. Guessing wastes time and creates technical debt.
-
RECOVERY CARDS ARE MANDATORY - Before compact: ALWAYS update the recovery card with CURRENT, ACCURATE information. After compact: EXECUTE the recovery commands IMMEDIATELY. Do not read random files. Do not ask questions. Run the commands provided.
-
DRY - DON'T REPEAT YOURSELF - ALWAYS design reusable components FIRST. Before touching ANY existing code, ask: "Will I need to make this same change in multiple places?" If YES, create a reusable component/function/mixin FIRST, then use it everywhere. NEVER scatter the same logic across multiple files. ONE source of truth. ONE place to change.
-
STOP. UNDERSTAND. SPEC. THEN CODE. - Before making ANY changes to UI components:
- STOP: Do not touch code yet
- UNDERSTAND: Map out the existing architecture. What components exist? How are they shared? What props do they use?
- SPEC: Document what the expected behavior should be in EACH state/mode BEFORE coding
- THEN CODE: Only after you have a clear spec, make changes
- If you find yourself adding props to HIDE functionality, you're doing it wrong. Ask first.
- If two pages should behave the same, they should use the SAME component with the SAME props.
- NEVER assume differences between pages are intentional. ASK.
What this means in practice:
- Found a test warning? FIX IT. Don't say "it still passes."
- Found bad code? FIX IT. Don't say "it was already there."
- Unsure of the right approach? RESEARCH IT. Don't guess.
- Taking longer than expected? THAT'S FINE. Correct code takes time.
- Running prepare-compact? UPDATE THE RECOVERY CARD WITH CURRENT INFO. Not stale info. CURRENT STATE.
- After compact with recovery prompt? EXECUTE THE COMMANDS IMMEDIATELY. Don't read random files. RUN THE COMMANDS.
- Adding a feature to multiple components? CREATE A REUSABLE COMPONENT FIRST. Don't scatter changes across files.
- Changing UI components? STOP. Map the architecture. Write a spec. Get approval. THEN code.
- Adding a prop to hide/disable functionality? STOP. Ask if the functionality should exist on both pages first.
- Two pages look different? Don't assume it's intentional. ASK before "fixing" by adding conditional props.
COPY THIS SECTION INTO EVERY RECOVERY FILE.
- Backend: Ruby 3.4.10, Rails 8.1.3.1, PostgreSQL 18
- Frontend: Vue 2.7.16 (14 separate instances), Bootstrap 4.6.2, Bootstrap-Vue 2.23.1
- Navigation: Turbolinks 5.2.0 with vue-turbolinks adapter
- Build: esbuild via jsbundling-rails, Propshaft asset pipeline
- Package Manager: YARN (NOT npm, NOT pnpm)
- Templates: HAML (NOT ERB)
- Settings: mitre-settingslogic gem with
config/vulcan.default.yml - Documentation: VitePress 2.x in
docs/(separate Vue 3 deps, isolated from Rails Vue 2)
# Development
foreman start -f Procfile.dev # Start dev server (Rails + esbuild watch)
bin/rails server # Rails only (no JS rebuild)
yarn build:watch # esbuild watch only
# Testing — ALWAYS use bin/parallel_rspec for full suite (3-4x faster)
bin/parallel_rspec spec/ # Full suite, capped at 8 cores
# NEVER use bare `bundle exec parallel_rspec` — it auto-detects all CPU cores
# and exceeds the 8 test databases, causing NoDatabaseError on workers 9+.
bundle exec rspec spec/path/to/spec.rb # Single file
bundle exec rspec spec/path/to/spec.rb:42 # Single test line
yarn test:unit # Vue component tests (Vitest)
# test-prof profiling (zero overhead when ENV vars not set)
TAG_PROF=type bundle exec rspec # Profile by test type
EVENT_PROF=factory.create bundle exec rspec # Factory usage profiling
EVENT_PROF=sql.active_record bundle exec rspec # SQL query profiling
FPROF=1 bundle exec rspec # Factory cascade detection
FPROF=flamegraph bundle exec rspec # Factory cascade flamegraph
# IMPORTANT: After running db:migrate, sync all parallel test databases
bundle exec rake parallel:prepare # Sync schema to all parallel test DBs
# Linting
bundle exec rubocop --autocorrect-all # Ruby/Rails
yarn lint # JavaScript/Vue (with --fix)
bundle exec brakeman # Security scanning
bundle exec bundler-audit # Gem vulnerability check
# Database
rails db:migrate
rails db:seed
rails db:reset # Drop, create, migrate, seed
bundle exec rake parallel:prepare # REQUIRED after any migration — syncs parallel test DBs
bundle exec rails stig_and_srg_puller:pull # Pull latest STIGs/SRGs
# Docker
./setup-docker-secrets.sh # Generate .env with secrets
docker compose build # Build image
docker compose up # Start full stack
docker compose down -v # Stop and remove volumes
# Upgrade toolkit (see docs/site/deployment/upgrade-guide.md)
bundle exec rake upgrade:preflight # Read-only report: version, pending actions, warnings
bundle exec rake upgrade:fix # Apply safe auto-fixable actions (DB renames, backfills)
bundle exec rake upgrade:verify # Validate post-upgrade state (migrations, DB names, env)
bundle exec rake upgrade:auto # Preflight + fix in one shot (used by entrypoints)
# Documentation (VitePress — runs from project root)
yarn docs:dev # Start VitePress dev server (hot reload)
yarn docs:build # Build static site to docs/.vitepress/dist/
yarn docs:preview # Preview production build locallyEach page has its own Vue instance mounted via a pack file in app/javascript/packs/. They do NOT share a router or global store - each is independent.
Entry points (configured in esbuild.config.js):
application.js- Base with Turbolinks/Rails UJS setupnavbar.js,toaster.js- Global UI (present on every page)login.js- Login pageprojects.js,project.js- Project managementproject_components.js,project_component.js- Component editingrules.js- Rule managementsecurity_requirements_guides.js,stig.js,stigs.js- SRG/STIG viewsusers.js- User management
All packs use vue-turbolinks mixin for Turbolinks compatibility. Components are in app/javascript/components/, shared utilities in app/javascript/store/ and app/javascript/mixins/.
Project- Container for components, has manyMembershiprecordsComponent- STIG-ready content with rules (belongs to Project + SRG)Rule- Individual security control (mapped to SRG requirement viarule_id)SecurityRequirementsGuide(SRG) - DISA baseline requirementsStig- Published STIGs for referenceUser- Authentication via Devise (local, GitHub, LDAP, OIDC)Membership- User-to-Project access (role-based)ProjectAccessRequest- Request to join a projectReview- Workflow review on a rule
Devise with four providers configured in config/initializers/devise.rb:
- Local (email/password)
- GitHub (omniauth-github)
- LDAP (gitlab_omniauth-ldap)
- OIDC (omniauth_openid_connect with auto-discovery)
Provider enable/disable controlled by environment variables via config/vulcan.default.yml.
Three methods, in priority order:
- Env vars:
VULCAN_ADMIN_EMAIL+VULCAN_ADMIN_PASSWORD- runs ondb:preparevialib/tasks/admin_bootstrap.rake - First-user-admin:
VULCAN_FIRST_USER_ADMIN=true-after_createcallback in User model with PostgreSQL advisory lock - Manual:
rails db:create_admin
app/lib/xccdf/ contains parsers for DISA XCCDF XML format. SRGs and STIGs are imported from XML files via upload or stig_and_srg_puller:pull rake task.
RSpec.describe 'Resource', type: :request do
before do
Rails.application.reload_routes! # Required for Rails 8 lazy loading
end
it 'requires authentication' do
post '/stigs', params: { file: file }
expect(response).to redirect_to(new_user_session_path)
end
context 'when authenticated' do
before { sign_in user } # Devise::Test::IntegrationHelpers
# ...
end
endlet! creates an implicit before hook that runs in definition order relative to explicit before blocks. Always define let! declarations BEFORE before blocks:
# CORRECT - admin created before sign_in triggers user creation
let!(:existing_admin) { create(:user, admin: true) }
before { sign_in user }
# WRONG - sign_in runs first, user becomes first-user-admin
before { sign_in user }
let!(:existing_admin) { create(:user, admin: true) }let_it_befor expensive setup (SRG parsing, component creation). Creates once per group, reuses across examples.- Global
refind: trueconfigured inrails_helper.rb— everylet_it_berecord gets a fresh AR instance per example. Never override withrefind: false. before_allfor setup that must run once per group (memberships, associations). Wrapped in a transaction that rolls back after the group.- No
@ivaraliases — uselet_it_benames directly in shared contexts. The dual-naming pattern (@p_admin = shared_p_admin) is deprecated. - Custom RuboCop cop
Vulcan/LetItBeRefindenforcesrefind: falseis never used.
- ENV mutation: Use
climate_controlgem, never bareENV['X'] = value - Record identity: Never use
Record.last— use scoped queries or response body IDs - Global state: Never
Audited.auditing_enabled = false— useModel.without_auditing { } - Shared fixtures: Never
ensure { record.update_columns }onlet_it_be— use local records - Spec file size: Target ~300 lines, hard cap 500. Use
/spec-split-reviewskill to split.
- Flat naming:
reviews_triage_spec.rb, notreviews/triage_spec.rb - Shared contexts in
spec/support/shared_contexts/ - Shared examples in
spec/support/shared_examples/ - Full testing guide:
docs/site/development/testing.md
- Dockerfile: Multi-stage on UBI 9 Minimal (base, build-base, build, development, production) with jemalloc + YJIT compiled from source
- docker-compose.yml: PostgreSQL + Vulcan web,
env_file: required: false - bin/docker-entrypoint: Rails standard pattern - runs
db:prepareon server start - setup-docker-secrets.sh: Generates
.envwith secure random secrets
setup-docker-secrets.sh -> .env (secrets)
docker compose up -> PostgreSQL starts -> health check passes
-> web starts -> docker-entrypoint runs db:prepare
-> admin:bootstrap hooks into db:prepare
-> Rails server starts on :3000
- Non-secret ENV defaults set in Dockerfile (auth, SSL, first-user-admin)
- Database credentials use variable substitution in docker-compose.yml:
${POSTGRES_PASSWORD:-postgres} - Secrets (SECRET_KEY_BASE, CIPHER_PASSWORD, CIPHER_SALT) must be provided externally
- No static secrets in config files
See ENVIRONMENT_VARIABLES.md for complete list. Key ones:
SECRET_KEY_BASE,CIPHER_PASSWORD,CIPHER_SALT- Required secretsDATABASE_URL- PostgreSQL connection (or individual POSTGRES_* vars)VULCAN_ENABLE_OIDC,VULCAN_OIDC_ISSUER_URL,VULCAN_OIDC_CLIENT_ID/SECRETVULCAN_ENABLE_LDAP,VULCAN_LDAP_*VULCAN_ENABLE_LOCAL_LOGIN,VULCAN_ENABLE_USER_REGISTRATIONVULCAN_FIRST_USER_ADMIN- First user becomes adminVULCAN_ADMIN_EMAIL,VULCAN_ADMIN_PASSWORD- Env var admin bootstrapRAILS_FORCE_SSL- Defaults to false for Docker quickstartRAILS_LOG_TO_STDOUT- Container logging
config/vulcan.default.yml is an ERB template that reads environment variables with fallback defaults. Accessed in code via Settings.section.key.
- RuboCop with autocorrect
- ESLint for JavaScript/Vue
- BundleCheck, RailsSchemaUpToDate
- YamlSyntax, JsonSyntax validation
- Format:
RECOVERY-v2.2.2-SESSION##.md - Location: Project root (NOT committed to git)
- Create before context drops below 15%
- Always create a NEW file - never overwrite existing ones
- Session numbers increment: SESSION136, SESSION137, etc.
- NEVER use
git add -Aorgit add .- add files individually - Run
git statusfirst to review changes - All tests and linting must pass before committing
- ALWAYS backup before destructive git operations
When rebasing or merging concurrent work from collaborators (Aaron + Will work the same branch), NEVER blindly take one side. Review BOTH implementations and cherry-pick the best code, tests, and patterns from each side. Compare both versions, keep the better tests regardless of author, use the better pattern regardless of who wrote it. If both sides can be improved during the merge, do that too. "Ours" or "theirs" as a default is lazy and loses good work.
DO NOT constantly suggest commits during feature development.
Commits should be complete, tested, documented, logical units of work.
Uncommitted changes mid-feature are NORMAL - recovery cards handle context preservation.
Only commit when:
- User explicitly requests it
- A complete feature set is finished and tested
- Before switching to unrelated work (if user agrees)
Do NOT suggest commits:
- Mid-feature development
- Before /compact
- After every small change
feat: Add admin bootstrap with advisory lock
- Added rake task for env var admin creation
- Protected first-user-admin with advisory lock
- Added 9 tests for bootstrap scenarios
Authored by: Aaron Lippold<lippold@gmail.com>
Prefixes: feat:, fix:, test:, docs:, refactor:, chore:
No Claude signatures in commits.
- No quick hacks - Every solution must be properly designed
- Research before implementing - Check how Discourse, GitLab, Mastodon handle the same problem
- No static secrets in config files - Use setup scripts or env vars
- Design top-to-bottom before coding - Don't bolt new features onto old architecture
- Fix root causes - Never test around bugs or modify tests just to pass
- Own all code - If tests fail, find and fix the actual problem
- When a component has a bad API, FIX THE API. Don't work around it. - If you find yourself fighting with a component's design (hiding default behavior, complex workarounds, passing props to disable features), stop and fix the component's API instead. Example: NewComponentModal rendering buttons by default was backwards - fixed by adding
showOpenerprop (default: false). Working around bad design wastes time and creates technical debt.
The OpenAPI spec at doc/openapi/ is the design document for Vulcan's complete REST API. It is multi-file (managed via Redocly CLI) with the bundled output at doc/openapi.yaml.
Rules:
- Every JSON-returning endpoint MUST have an OpenAPI path file — if a route returns JSON but has no spec, that's an API gap to fix
- Every OpenAPI path MUST have a contract test in
spec/contracts/— validates real responses against the schema - API response changes and schema updates are ONE task — never change a controller render without updating the spec
- Use
yarn openapi:bundle && yarn openapi:lintafter any spec changes - Use
yarn openapi:docsto regenerate the VitePress API reference JSON - See
doc/openapi/CLAUDE.mdfor full documentation standards
Never use heredocs directly in bd commands - they corrupt .claude/settings.local.json.
# CORRECT - write to temp file first
cat > /tmp/desc.md <<'EOF'
... long content ...
EOF
bd update <id> --description "$(cat /tmp/desc.md)"
rm /tmp/desc.md- Lazy route loading requires
Rails.application.reload_routes!in test setup - Controller specs no longer work - use request specs
- Built-in reloader replaces Spring gem
- Turbolinks still works but is deprecated
- Don't use
git add -Aorgit add . - Don't rewrite working code without reason
- Don't add code without tests
- Don't use npm or pnpm - this project uses YARN
- Don't prioritize "fast" over "correct"
- Don't create placeholder/skeleton code and call it "complete"
- Don't ignore existing codebase patterns - find similar code and match it
- NEVER delete ANY files without explicit user permission - ask first, always
CRITICAL: Tests that verify what code DOES (instead of what it SHOULD DO) are worthless.
The Microsoft Outlook Disaster Pattern:
- Look at code: "right-panels is inside v-if"
- Write test: "expect right-panels NOT to render when no rule selected"
- Test passes ✓
- Code is BROKEN - panels should ALWAYS render
- Test encoded the BUG as expected behavior
Before writing ANY test, ask:
- What is the REQUIREMENT? (Not "what does the code do?")
- What SHOULD happen from the user's perspective?
- Would this test FAIL if the code had a bug?
Test Writing Rules:
- Document requirements in test file comments FIRST
- Write test for the requirement
- If test passes immediately, BE SUSPICIOUS - did you just test the implementation?
- MANUALLY VERIFY behavior in browser/UI - don't trust green checkmarks
- Use
mountnotshallowMountfor integration tests - test real component interaction
Signs your tests are worthless:
- You wrote tests AFTER the code and they all passed first try
- Tests use the same logic as the implementation
- You never manually verified the feature works
- Tests break when you refactor but bugs slip through
The rule: If your test would pass with buggy code, it's not testing anything.