Hydra tutorial series — Part 5: Starting a Hydra run on a real app
The full recipe from credentials to a first run: credentials from your environment, the HYDRA_FORGE setting, local images, one stage with dev-run.sh or the whole chain with smoke-test.sh, and a real run on a target app started with a label. Fifth of seven short modules.
The first four parts were about concept, pipelines, gates and skills. Time to actually run. On your own machine you run Hydra's stages as containers: one stage at a time, or the whole chain against a throwaway repo. A real run on a real app starts with a label, and the flows from part 7 carry it from there. This part covers both, with the practical pitfalls: credentials, the forge setting, and images.
Step 1: clone and look around
git clone https://github.com/ConductionNL/hydra.git
cd hydra
cat CLAUDE.md
CLAUDE.md is the canonical architecture doc. Worth scrolling through the first time. The secrets/ folder is not in git: you create secrets/.env yourself in step 3.
Step 2: credentials come from your environment
Hydra's local scripts read exactly two credentials, and only from environment variables:
export GH_TOKEN=$(gh auth token) # forge token; GIT_TOKEN is accepted as an alias
export CLAUDE_CODE_OAUTH_TOKEN=<token> # from `claude setup-token`; or ANTHROPIC_API_KEY=<key>
There is no credentials file. scripts/lib/env-credentials.sh is the one resolver, and it fails closed: when a variable is missing, the script stops with a reason instead of starting a run that dies three seconds later with a 401. The scripts pass both values to the containers by variable name (-e GH_TOKEN), so the values never end up in the process list or in a file.
Before the move to flows (2026), Hydra read secrets/credentials.json: a list of Claude tokens plus one git token per persona, with automatic rotation on rate-limit. That file, scripts/lib/credentials.sh and the rotation are gone. A leftover credentials.json on disk changes nothing.
Step 3: secrets/.env
The non-credential overrides. Different for production than for your laptop.
# Which forge the scripts and containers talk to. Required: there is no default.
HYDRA_FORGE=github
# Which images and tag to use. Locally you often build with -t localhost/hydra-*:test.
HYDRA_IMAGE_PREFIX=localhost/hydra
HYDRA_IMAGE_TAG=test
# Default review scope (ADR-020): PR diff only. Set to 'full' only when onboarding a repo.
HYDRA_REVIEW_SCOPE=diff
HYDRA_FORGE=github is right for every ConductionNL repo; forgejo is only for a customer who runs a self-hosted Forgejo. It has no default because guessing a forge means sending your token to a third party. secrets/.env.example sets it to github; if you copied an older example, check that line, because older copies set it to codeberg.
Tip: the scripts that read HYDRA_IMAGE_* (manual-review.sh, cron-audit.sh, cron-spec-from-issue.sh) fall back to localhost/hydra and test when the variables are unset, and dev-run.sh always starts localhost/hydra-<stage>:test, whatever you set. So locally you build the images yourself (step 4), or pull a published ghcr.io/conductionnl/hydra-<stage> image and tag it under the local name.
scripts/dev-run.sh refuses to start without this file and loads it itself, and so do reconcile.sh and the cron-*.sh sweeps. For a while dev-run.sh only checked that the file existed and never loaded it, so HYDRA_FORGE=github looked configured while every container still talked to Codeberg (hydra#391). Every entrypoint that reads configuration MUST source this file at startup; that's a repo-wide contract.
Step 4: build the container images
In CI, the hydra-image.yml workflow builds the base, builder, reviewer and security images and pushes them to GHCR as ghcr.io/conductionnl/hydra-*. Locally you build them yourself, under the names the scripts look for:
# Base image: Nextcloud + PostgreSQL + OpenRegister (builder dependency)
docker build -t ghcr.io/conductionnl/hydra-nextcloud-test:stable32 \
-f images/nextcloud-test/Dockerfile .
# The three pipeline images
docker build -t localhost/hydra-builder:test -f images/builder/Dockerfile .
docker build -t localhost/hydra-reviewer:test -f images/reviewer/Dockerfile .
docker build -t localhost/hydra-security:test -f images/security/Dockerfile .
# Once: the bridge network the containers run on
docker network create hydra-net
Then check with docker images | grep hydra that the three tags are there. Reviewer and security are standalone (no NC dependency). The builder copies the Nextcloud tree and its database out of ghcr.io/conductionnl/hydra-nextcloud-test:stable32 with COPY --from, so that image has to exist under exactly that name before you build the builder.
Step 5: run one stage with dev-run.sh
dev-run.sh starts one container directly, with no board and no flow around it. That is the quickest way to see what a persona does:
# Builder: implement an OpenSpec change on a branch and open a PR
./scripts/dev-run.sh openspec/changes/<change> builder \
--repo-url https://github.com/ConductionNL/<app>
# Code review or security review on an existing PR
./scripts/dev-run.sh - reviewer --repo-url https://github.com/ConductionNL/<app> \
--pr-url https://github.com/ConductionNL/<app>/pull/<N>
./scripts/dev-run.sh - security --repo-url https://github.com/ConductionNL/<app> \
--pr-url https://github.com/ConductionNL/<app>/pull/<N>
The persona's output streams to your terminal, and its commits and comments land on the PR.
Step 6: the whole chain on a throwaway repo
To see builder, reviewer and security run back to back, use smoke-test.sh. It works on a disposable repo under your own GitHub account, so nothing on a real app is touched:
./scripts/smoke-test.sh --spec-dir /tmp/todo-mvp-spec \
--github-user <your-github-user> --repo-name todo-app
The script renames an older todo-app to todo-app-runN, creates a fresh one with the spec uploaded, builds the images, runs the builder, then runs reviewer and security in parallel, and prints a summary. The spec directory needs a design.md, a tasks.md and a specs/ folder. From Claude Code, /local-run does the same in one command.
Step 7: a real run on a target app
On a real app you don't start containers yourself. You put the trigger label build:queued on the issue and the flows do the rest. Two things have to be true first:
- The issue body names the change as a path,
openspec/changes/<name>. The sequencer takes the first such path it finds. No path meansbuild:blocked. - That change directory exists on the base branch:
developmentif the repo has one, otherwise the default branch. If it doesn't, the result isbuild:blockedas well.
Then set the label via the Hydra helper:
./scripts/hydra-label.sh ConductionNL/<app> <N> add build:queued
scripts/hydra-label.sh is the preferred path: it uses the label helpers from scripts/lib/labels.sh and syncs the GitHub Project board in real time. A direct gh issue edit --add-label works too, but then the board stays in the old column until reconcile (every 10 minutes) picks it up.
The hydra-sequencer flow finds the label on one of its five-minute ticks. Each tick it searches the org for open issues with a queued label and does one unit of work; review labels go before build:queued, so a busy queue can take a few ticks to reach yours. Its definition ships with enabled: false, so this only happens on an OpenRegister instance where the flow has been switched on. Part 7 covers that setup.
Older versions of this part used ready-to-build as the trigger and yolo for an auto-merge. create-labels.sh still creates both labels, but no flow reads either of them. The trigger is build:queued, and nothing merges automatically.
Step 8: follow the pipeline
Once the sequencer has the issue, the labels move like this:
build:queued → build:pass | build:fail | build:no-change | build:blocked
(a PR is opened on pass and on fail; it is never merged)
build:pass → you add code-review:queued yourself
code-review:queued → code-review:pass (+ security-review:queued) | code-review:fail | needs-input
security-review:queued → security-review:pass (+ applier:queued) | security-review:fail | needs-input
applier:queued → applier:pass | applier:fail | needs-input
A few things that are easy to expect and do not happen:
- Nothing queues code review after
build:pass. Add it yourself:./scripts/hydra-label.sh ConductionNL/<app> <N> add code-review:queued. - There are no
:runninglabels. While a stage runs, its:queuedlabel stays on the issue and a lock marks the issue as in flight. - A
failstops the chain. A failed code review does not go on to security review. applier:passis the end. There is nodonelabel and no merge; a human merges the PR.
You follow it in two places:
- GitHub issue: the labels change as the sequencer advances the stages. The sequencer writes those labels itself, so the project-board card does not follow straight away;
reconcile.shmoves it on its next sweep. - GitHub PR: the build commit on
feature/<issue>/hydra-build, with the gate output in the PR body. Each review stage writes its verdict to ahydra-verdict.jsonfile that the sequencer collects on the next tick; that verdict is what moves the labels.
Behind both, after each stage's labels have moved, the sequencer records the stage in openspec/changes/<slug>/hydra.json through the hydra-record-stage flow, which refuses a stage that names no owner. Part 6 shows how to read that file. The flow runs themselves live in OpenRegister; there is no log file on your machine to tail.
Troubleshooting
First-run problems
The script stops straight away with a missing-credential errorExport GH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN (or ANTHROPIC_API_KEY) in the same shell you start the script from. The resolver never reads a file, so a secrets/credentials.json does not help.
dev-run.sh says secrets/.env not foundCreate the file from step 3. It has to be in secrets/ at the root of the hydra repo, not in your home dir: the scripts read it from a fixed path relative to the repo root.
A container refuses to start because HYDRA_FORGE is unsetAdd HYDRA_FORGE=github to secrets/.env. The refusal is deliberate: without it, Hydra would have to guess where to send your token.
A script starts a different image than the one I builtdev-run.sh always uses localhost/hydra-<stage>:test and ignores HYDRA_IMAGE_*, so build with exactly those tags. For manual-review.sh and the cron scripts, check what HYDRA_IMAGE_PREFIX and HYDRA_IMAGE_TAG in secrets/.env resolve to; unset, they mean localhost/hydra and test.
Build starts, no image found errordocker images | grep hydra-builder. No hit? Build the base image first (hydra-nextcloud-test) and then the builder. The builder Dockerfile copies from the base with COPY --from, so the base has to exist under the name ghcr.io/conductionnl/hydra-nextcloud-test:stable32. A network hydra-net not found error means you skipped the last line of step 4.
The label is on the issue, but nothing happensNothing on your machine watches labels. On a real app the hydra-sequencer flow has to be switched on in the OpenRegister instance (part 7). Check the label's exact name as well: with HYDRA_LABEL_PREFIX set, hydra-label.sh writes <prefix>-build:queued, which no flow reads. And the sequencer does one unit of work per tick, review stages first, so a full queue delays your build. To try a stage locally, use dev-run.sh from step 5 instead.
The issue gets build:blocked right awayThe sequencer could not find the change. The issue body has to contain openspec/changes/<name>, and that directory has to exist on development (or on the default branch, if the repo has no development). Fix the body or push the change, then remove build:blocked and add build:queued again.
A review runs, or needs-input appears, although I only queued a buildStale review labels from a previous run are still on the issue, and the sequencer takes those before build:queued. Reset the issue with ./scripts/hydra-label.sh ConductionNL/<app> <N> transition build:queued: that strips every other queued label, needs-input and the old build verdict in one call, and leaves only build:queued as the starting state.
Test yourself
Four short questions to check whether you've grasped this part. Stuck? Click Hint. Curious about the answer? Click Answer.
1. Where do Hydra's local scripts get their credentials, and what happens when one is missing?
Hint
Two variables, one resolver. Does it fall back to anything?
Answer
From the environment only: GH_TOKEN (or its alias GIT_TOKEN) for the forge, and CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY for the model. scripts/lib/env-credentials.sh fails closed: a missing variable stops the script with an explicit reason and a non-zero exit. It never reads a file, so there is nothing to fall back to, and a stale credentials.json on disk changes nothing.
2. Why does HYDRA_FORGE have no default?
Hint
What does a script do with your token once it has decided which forge to talk to?
Answer
Guessing a forge means sending the run's credentials to a third party. It used to default to codeberg, and that is how a GitHub token once ended up at codeberg.org. Now the scripts refuse to start until you say github (every ConductionNL repo) or forgejo (a customer's self-hosted Forgejo).
3. Why call ./scripts/hydra-label.sh instead of gh issue edit --add-label when triggering a run?
Hint
The labels are the source of truth, but there is a second thing that has to follow them.
Answer
hydra-label.sh goes through the label helpers in scripts/lib/labels.sh, and every one of those ends by syncing the GitHub Project board. So one call changes the label and moves the card. gh issue edit changes only the label; the board stays in the old column until reconcile.sh (every 10 minutes) catches the drift.
4. Your smoke-test.sh chain came back green. What have you proven, and what not?
Hint
What does a run leave behind when no flow is involved?
Answer
Proven: the builder, reviewer and security stages work on this spec, with these images and these credentials.
Not proven: anything about sequencing or attribution. smoke-test.sh and dev-run.sh start containers directly, so no hydra.json stage record is written, no owner is attributed, and no label state machine is exercised. Claims of the form "the stage records prove it" need a run through the flows.
Next step
Pipeline running? Great. But what do you do when it goes wrong? Part 6 covers recovery and escalation patterns.