Skip to content

Configuration

Catalyst uses a two-layer configuration system that keeps secrets out of git while allowing project metadata to be shared with your team. The setup script (setup-catalyst.sh) generates both layers automatically.

Safe to commit. Contains non-sensitive project metadata that Catalyst reads to understand your project structure, ticket conventions, and workflow state mapping.

{
"catalyst": {
"projectKey": "acme",
"repository": {
"org": "acme-corp",
"name": "api"
},
"project": {
"ticketPrefix": "ACME",
"name": "Acme Corp API"
},
"linear": {
"teamKey": "ACME",
"stateMap": {
"backlog": "Backlog",
"todo": "Todo",
"research": "In Progress",
"planning": "In Progress",
"inProgress": "In Progress",
"inReview": "In Review",
"done": "Done",
"canceled": "Canceled"
}
},
"thoughts": {
"user": null
}
}
}
FieldTypeDescription
catalyst.projectKeystringLinks to the secrets config file (config-{projectKey}.json)
catalyst.repository.orgstringGitHub organization
catalyst.repository.namestringRepository name
catalyst.project.ticketPrefixstringLinear ticket prefix (e.g., “ACME”)
catalyst.project.namestringHuman-readable project name
catalyst.linear.teamKeystringLinear team identifier used in ticket IDs (e.g., “ACME” for ACME-123). Must match ticketPrefix.
catalyst.linear.stateMapobjectMaps workflow phases to your Linear workspace state names
catalyst.thoughts.userstring|nullHumanLayer thoughts user name

The stateMap controls automatic Linear status updates as you move through the development workflow:

KeyUpdated WhenDefault
backlogInitial ticket stateBacklog
todoAcknowledged, unstartedTodo
researchRunning research-codebaseIn Progress
planningRunning create-planIn Progress
inProgressRunning implement-planIn Progress
inReviewRunning create-pr or describe-prIn Review
doneRunning merge-prDone
canceledManual cancellationCanceled

Set any key to null to skip that automatic transition.

stateMap values are auto-detected from Linear — when you run setup-catalyst.sh with a Linear API token, the script fetches your team’s actual workflow states and populates stateMap with the correct names. Manual customization is only needed for non-standard state names.

In most teams, the intended meaning is:

  • research — Catalyst is still understanding the problem and the current code
  • planning — the implementation approach is being written and reviewed
  • inProgress — code changes are actively being made
  • inReview — a PR exists and is being worked through review and CI
  • done — the PR has merged

This is useful because the PR stage is not just “waiting on somebody else.” In Catalyst’s model, inReview still includes active follow-up work such as fixing CI, addressing automated review feedback, updating the PR description, and re-checking merge readiness.

Catalyst can open PRs, watch checks, address review comments, and try to merge safely. But GitHub decides what is actually required before main can be merged into.

Those merge requirements live in GitHub branch protection or repository rulesets, not in .catalyst/config.json.

If you want GitHub to block merges until review is complete, configure that in GitHub:

  • require pull requests for main
  • require status checks before merge
  • require one or more approving reviews
  • require conversation resolution if review threads must be closed
  • optionally enable auto-merge once those requirements pass

Catalyst should behave as if these gates matter, but only GitHub can enforce them.

For most teams using Catalyst, the best default is autonomous mode: let Catalyst work the PR to completion, but make GitHub enforce the quality gates around checks and unresolved review comments.

  • Enable pull requests.
  • Enable squash merge.
  • Enable auto-merge.
  • Enable automatic deletion of head branches after merge.
  • Set the default branch to main.

Target refs/heads/main with an active branch ruleset that:

  • blocks direct deletion
  • blocks non-fast-forward pushes
  • requires pull requests for changes into main
  • requires review conversations to be resolved before merge
  • requires status checks to pass before merge

For autonomous mode, set:

  • required approving reviews: 0
  • required review thread resolution: true
  • required status checks: true

This gives you a fully automated merge path where Catalyst can:

  • open the PR
  • wait for checks and bot comments
  • fix actionable feedback
  • resolve review threads
  • merge once the PR is genuinely clean

without waiting for a human approval click.

For this repo shape, the recommended required check currently enabled in GitHub is:

  • Cloudflare Pages

Once your repository runs the following checks on every PR to main, you should add them as required checks too:

  • audit-references
  • check-versions
  • validate

Cloudflare Pages covers preview deploy readiness. The other three checks are repository-owned guardrails:

  • audit-references catches broken plugin references
  • check-versions verifies plugin changes are releasable through Release Please
  • validate checks release configuration consistency

If your repository has additional always-on checks, add them too. The important rule is: only mark a check as required if it runs on every PR to main.

If you want a human signoff before merge, keep everything above and additionally set:

  • required approving reviews: 1 or more

That changes the operating model from autonomous shipping to human-approved shipping. Catalyst still does the same review-follow-up work, but GitHub will not allow the merge until a human reviewer approves it.

The recommended operating model is:

  • automated reviewers can leave comments and request fixes
  • Catalyst should address actionable review feedback and resolve threads
  • GitHub should block merge until required conversations and checks are complete
  • human approval should be optional and controlled by the repository owner, not assumed by Catalyst

Catalyst can do the work of:

  • opening the PR
  • waiting for checks
  • reading bot and human review comments
  • fixing code
  • updating the PR
  • attempting the merge once the PR is clean

But the repository settings are what make those expectations enforceable for every contributor, not just when Catalyst happens to be driving.

Secrets Config (~/.config/catalyst/config-{projectKey}.json)

Section titled “Secrets Config (~/.config/catalyst/config-{projectKey}.json)”

Never committed. One file per project, linked by projectKey.

{
"catalyst": {
"linear": {
"apiToken": "lin_api_...",
"teamKey": "ACME",
"defaultTeam": "ACME"
},
"sentry": {
"org": "acme-corp",
"project": "acme-web",
"authToken": "sntrys_..."
},
"posthog": {
"apiKey": "phc_...",
"projectId": "12345"
},
"exa": {
"apiKey": "..."
}
}
}
IntegrationRequired FieldsUsed By
LinearapiToken, teamKeycatalyst-dev, catalyst-pm
Sentryorg, project, authTokencatalyst-debugging
PostHogapiKey, projectIdcatalyst-analytics
ExaapiKeycatalyst-dev (external research)

Only configure the integrations you use. The setup script prompts for each one.

Monitor OTel Config (~/.config/catalyst/config.json)

Section titled “Monitor OTel Config (~/.config/catalyst/config.json)”

The orchestration monitor reads OpenTelemetry backend endpoints from a global config file at ~/.config/catalyst/config.json. This file is separate from the per-project secrets files.

{
"otel": {
"enabled": true,
"prometheus": "http://localhost:9090",
"loki": "http://localhost:3100"
}
}
FieldTypeDefaultDescription
otel.enabledbooleanfalseEnable OTel proxy endpoints on orch-monitor
otel.prometheusstringnullPrometheus query URL (for /api/otel/query)
otel.lokistringnullLoki query URL (for /api/otel/logs)

Environment variable overrides: OTEL_ENABLED, PROMETHEUS_URL, LOKI_URL. Env vars take precedence over the file when both are set.

If you’re running the claude-code-otel Docker Compose stack locally, the defaults above match the standard ports. For hosted backends (Grafana Cloud, Datadog, etc.), point these URLs at your hosted Prometheus/Loki-compatible endpoints.

See Setting up the OTel stack for the full installation guide.

The monitor dashboard supports AI-powered status summaries. Configuration spans both layers:

Project config (.catalyst/config.json) — opt-in toggle:

{
"catalyst": {
"ai": {
"enabled": true
}
}
}

Secrets config (~/.config/catalyst/config-{projectKey}.json) — provider credentials:

{
"ai": {
"gateway": "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}",
"provider": "anthropic",
"model": "claude-haiku-4-5-20251001",
"apiKey": "sk-ant-..."
}
}
FieldRequiredDefaultDescription
ai.enabledYes (project config)falseMaster toggle. No API calls when off.
ai.gatewayYes (secrets)Cloudflare AI Gateway URL
ai.providerNoanthropicAI provider: anthropic or openai
ai.modelNoclaude-haiku-4-5-20251001Model ID
ai.apiKeyYes (secrets)Provider API key

The AI briefing generates a natural-language status summary and suggests session labels based on Linear ticket context. It is on-demand (button click) or optionally auto-refreshing. Zero cost when disabled.

Define the commands that run when creating a new worktree via /create-worktree or /orchestrate. This replaces the default auto-detected setup (dependency install + thoughts init) with full project control — like conductor.json’s lifecycle hooks.

{
"catalyst": {
"worktree": {
"setup": [
"humanlayer thoughts init --directory ${DIRECTORY} --profile ${PROFILE}",
"humanlayer thoughts sync",
"bun install"
]
}
}
}

Commands run in order, inside the new worktree directory. Each command supports variable substitution:

VariableValue
${WORKTREE_PATH}Absolute path to the new worktree
${BRANCH_NAME}Git branch name
${TICKET_ID}Same as branch name
${REPO_NAME}Repository name
${DIRECTORY}Thoughts directory (from catalyst.thoughts.directory or repo name)
${PROFILE}Thoughts profile (from catalyst.thoughts.profile or auto-detected)

If catalyst.worktree.setup is not configured, the script falls back to auto-detected setup: make setup or bun/npm install, then humanlayer thoughts init + sync. Once you define setup, only your commands run — the auto-detection is skipped entirely.

Catalyst now pre-trusts newly created worktrees in Claude Code automatically, so you do not need to add a separate trust-workspace.sh command to your setup array.

Optional. Add this block to enable /orchestrate — see Orchestration for full documentation.

{
"catalyst": {
"orchestration": {
"worktreeDir": null,
"maxParallel": 3,
"hooks": {
"setup": ["bun install"],
"teardown": []
},
"workerCommand": "/oneshot",
"workerModel": "opus",
"testRequirements": {
"backend": ["unit"],
"frontend": ["unit"],
"fullstack": ["unit"]
},
"verifyBeforeMerge": true,
"allowSelfReportedCompletion": false
}
}
}
FieldTypeDefaultDescription
worktreeDirstring|null~/catalyst/wt/<projectKey>Base directory for worktrees
maxParallelnumber3Max concurrent workers
hooks.setupstring[][]Run after worktree creation (supports ${WORKTREE_PATH}, ${BRANCH_NAME}, ${TICKET_ID}, ${REPO_NAME}, ${DIRECTORY} variables)
hooks.teardownstring[][]Run before worktree removal
workerCommandstring/oneshotSkill to dispatch in each worker
workerModelstringopusModel for worker sessions
testRequirementsobjectSee aboveRequired test types by scope (backend/frontend/fullstack)
verifyBeforeMergebooleantrueRun adversarial verification before allowing merge
allowSelfReportedCompletionbooleanfalseTrust worker’s self-reported completion without verification

Optional. Controls where orchestrator artifacts are persisted and how long they are retained. The archive is a hybrid SQLite index plus filesystem blob store written by catalyst-archive (see ADR-009).

Goes in the global user config at ~/.config/catalyst/config.json:

{
"archive": {
"root": "~/catalyst/archives",
"syncToThoughts": false,
"retention": { "days": 90 }
}
}
FieldTypeDefaultDescription
rootstring~/catalyst/archivesRoot directory for archived blobs. One subdirectory per orchestrator id.
syncToThoughtsbooleanfalseWhen true, catalyst-archive sweep also copies the top-level SUMMARY.md to thoughts/shared/handoffs/.
retention.daysnumber|nullnull (no prune)Default threshold for catalyst-archive prune when --older-than is not supplied.

Environment variables override these paths when set:

  • CATALYST_ARCHIVE_ROOT — overrides archive.root
  • CATALYST_RUNS_DIR — orchestrator runtime source (default ~/catalyst/runs)
  • CATALYST_DB_FILE — SQLite index path (default ~/catalyst/catalyst.db)
  • CATALYST_COMMS_DIR — catalyst-comms source (default ~/catalyst/comms/channels)

The archive root is created on first sweep and tolerates missing optional artifacts (e.g., a worker without a rollup fragment). Re-running the sweep is idempotent (all upserts).

Workflow Context (.catalyst/.workflow-context.json)

Section titled “Workflow Context (.catalyst/.workflow-context.json)”

Auto-managed by Claude Code hooks and skills. Not committed to git.

{
"lastUpdated": "2025-10-26T10:30:00Z",
"currentTicket": "PROJ-123",
"orchestration": null,
"mostRecentDocument": {
"type": "plans",
"path": "thoughts/shared/plans/...",
"created": "2025-10-26T10:30:00Z",
"ticket": "PROJ-123"
},
"workflow": {
"research": [],
"plans": [],
"handoffs": [],
"prs": []
}
}
FieldTypeDescription
currentTicketstring | nullActive ticket ID for this worktree
orchestrationstring | nullOrchestration run name (set by create-worktree.sh --orchestration). Groups orchestrator + workers for per-run telemetry via catalyst.orchestration OTel resource attribute.

This file is what enables skill chaining — when you save research, create-plan finds it automatically. When you save a plan, implement-plan finds it. You never need to specify file paths between workflow phases.

The workflow-context.sh script manages this file programmatically:

Terminal window
workflow-context.sh init # Create file if missing
workflow-context.sh set-ticket PROJ-123 # Set currentTicket (no document needed)
workflow-context.sh set-orchestration NAME # Set orchestration run name
workflow-context.sh add research "path" "PROJ-123" # Add document + set ticket
workflow-context.sh recent research # Get most recent document of type
workflow-context.sh most-recent # Get most recent document (any type)
workflow-context.sh ticket PROJ-123 # Get all documents for a ticket

The workflow context file is created automatically at several points:

  • Skill prerequisites — all workflow skills call check-project-setup.sh which runs workflow-context.sh init
  • Worktree creationcreate-worktree.sh initializes the file and sets currentTicket from the worktree name (e.g., worktree ENG-123 sets ticket to ENG-123)
  • Ticket-based skills/oneshot PROJ-123 calls set-ticket immediately after parsing the ticket, before any research begins

The workflow context file is also read by direnv to populate OTEL_RESOURCE_ATTRIBUTES with the current ticket. This enables per-ticket telemetry correlation in Claude Code’s native OpenTelemetry support.

Setup: Add a .envrc to your repo root:

Terminal window
source_up
use_otel_context "your-project-name"

The use_otel_context function (from ~/.config/direnv/lib/otel.sh) sets these OTEL resource attributes:

AttributeSource
projectArgument to use_otel_context
hostnameMachine short name
git.branchCurrent git branch
linear.keyTicket from branch name, fallback to currentTicket in workflow context

source_up inherits environment from parent .envrc files (e.g., profile-based secrets at the workspace root). When using worktrees, create-worktree.sh generates a .envrc and runs direnv allow automatically.

direnv is recommended when working across multiple repositories. It automatically loads per-directory environment variables, keeping API keys isolated between projects and populating OTel resource attributes for observability.

Terminal window
brew install direnv

Add the shell hook to your profile (~/.zshrc or ~/.bashrc):

Terminal window
eval "$(direnv hook zsh)" # or bash

Catalyst ships two direnv library functions. Install them to ~/.config/direnv/lib/ so they’re available in all .envrc files:

use_profile — loads environment variables from a named profile file:

~/.config/direnv/lib/profiles.sh
# Loads vars from ~/.config/direnv/profiles/{name}.env
# Later profiles override earlier ones.

use_otel_context — sets OTEL_RESOURCE_ATTRIBUTES for telemetry correlation:

~/.config/direnv/lib/otel.sh
# Sets project, hostname, git.branch, linear.key, catalyst.orchestration

Create profile files at ~/.config/direnv/profiles/ to separate credentials by project:

~/.config/direnv/profiles/
├── personal.env # Global defaults (Cloudflare, AWS, PostHog)
├── adva.env # Client-specific keys (Supabase, Postmark, geocoding APIs)
├── slides.env # Project-specific keys (ElevenLabs, Gemini TTS)
└── accounting.env # Project-specific keys (Wave, Monarch)

Each file is a simple KEY=value format — no export prefix needed (direnv handles that).

Each project root gets an .envrc file that layers profiles and sets OTel context:

~/code-repos/github/acme/project/.envrc
use_profile personal # Base credentials
use_profile acme # Client-specific overrides
use_otel_context "acme" # OTel resource attributes

Sub-directories (e.g., Conductor workspaces or worktrees) inherit from the parent:

~/conductor/workspaces/acme/workspace-1/.envrc
source_up # Inherit from parent .envrc
use_otel_context "acme" # OTel context for this workspace

The source_up directive walks up the directory tree until it finds a parent .envrc, chaining configurations. This means worktrees and Conductor workspaces automatically get the parent project’s API keys without duplicating them.

Without direnv, API keys end up in shell profiles (.zshrc) where they’re global — every project sees every key. With direnv profiles:

  • Credentials are scopedcd into a project and only its keys are loaded
  • OTel attributes are automatic — every Claude Code session gets the right project and linear.key labels without manual configuration
  • Worktrees inheritsource_up means new worktrees get the right environment immediately
  • No secret leakage.envrc files are committed (they reference profiles, not secrets); profile .env files are local-only

The thoughts system provides git-backed persistent context across sessions. The setup script handles initialization, but for manual setup:

Terminal window
cd /path/to/your-project
humanlayer thoughts init
# Or with a specific profile for multi-project isolation
humanlayer thoughts init --profile acme

Directory structure:

<org_root>/
├── thoughts/ # Shared by all org projects
│ ├── repos/
│ │ ├── project-a/
│ │ │ ├── {your_name}/
│ │ │ └── shared/
│ │ └── project-b/
│ └── global/
├── project-a/
│ └── thoughts/ # Symlinks to ../thoughts/repos/project-a/
└── project-b/
└── thoughts/ # Symlinks to ../thoughts/repos/project-b/
Terminal window
humanlayer thoughts sync # Sync changes
humanlayer thoughts status # Check status
humanlayer thoughts sync -m "Updated research" # Sync with message
# Back up to GitHub
cd <org_root>/thoughts
gh repo create my-thoughts --private --source=. --push

Change projectKey in .catalyst/config.json to point to a different secrets file:

{
"catalyst": {
"projectKey": "work"
}
}

For fully isolated multi-client setups, see Multi-Project Setup.

  1. File exists: ls .catalyst/config.json
  2. Valid JSON: cat .catalyst/config.json | jq
  3. Correct location: must be in the .catalyst/ directory (or .claude/ for backward compat)
  4. Secrets file exists: ls ~/.config/catalyst/config-{projectKey}.json
Terminal window
humanlayer thoughts status
humanlayer thoughts init # Re-initialize if needed