Skip to content

Authenticate the pipeline container by login or by token - #853

Closed
Jack-Edwards wants to merge 45 commits into
stablefrom
chore/pipeline-oauth-token
Closed

Authenticate the pipeline container by login or by token#853
Jack-Edwards wants to merge 45 commits into
stablefrom
chore/pipeline-oauth-token

Conversation

@Jack-Edwards

@Jack-Edwards Jack-Edwards commented Aug 6, 2026

Copy link
Copy Markdown
Member

Claude Code in the pipeline container authenticates from an interactive login held in the crypter-pipeline-claude volume. Renewing it when it lapses meant knowing to open a shell in the container and run claude there.

pipeline.sh login makes that a command. CLAUDE_CODE_OAUTH_TOKEN in .devcontainer/.env is an alternative for a machine that would rather configure the credential than open a browser; Compose reads it into the container's environment, where Claude Code uses it in place of the stored login.

Nothing is required of you after merging — an existing login keeps working. up warns when neither a token nor a login is configured, rather than refusing.

🤖 Generated with Claude Code

Jack-Edwards and others added 30 commits August 2, 2026 21:20
The pipeline agents run unattended, so they run in a container with no access
to the host filesystem or host credentials. The image carries the toolchain a
Crypter build needs -- .NET 10, pnpm pinned to the version CI uses, wasm-tools,
dotnet-ef -- plus git, gh, and the Claude Code CLI.

Docker is deliberately absent. Handing the container a Docker socket would hand
back the host, so Crypter.Test cannot run inside the loop; the tests run in CI
once the pull request exists.

The agents run as uid 1001 rather than reusing the base image's uid 1000. That
account belongs to the sudo group, and while sudo is not installed today, a
derived image that installed it would silently grant the agents root.

The workflow publishes to GHCR on pushes to stable that touch .devcontainer,
and needs a DEVCONTAINER_IMAGE_NAME repository variable to exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each stage of the pipeline runs as its own subagent so that it starts from a
clean context. A stage that inherits the reasoning behind the work it is
checking tends to ratify it instead of examining it.

Tool grants do the enforcing. The conformance auditor and the reviewer cannot
write code, so a deviation or a defect they find has to be reported rather than
quietly repaired. The implementer is reused for the initial build, for accepted
review findings, and for CI fixes, so there is no fourth definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skill drives a requirement from plan to a published pull request: sync and
branch, plan, implement, open the pull request as a draft, audit and review in
parallel, remediate, then publish and hold the pull request against CI. The
plan is the one place it stops for a human.

The CI loop lives here rather than in the publisher agent. A subagent cannot
invoke another subagent, so the publisher runs a single attempt and reports what
CI said, and the skill counts attempts and calls the implementer between them.
That also means each attempt reads the current state of the checks instead of
reasoning from its own earlier diagnosis. Three attempts, then it hands back.

Everything happens on a fork, so nothing the agents run can reach this
repository. Run state under .claude/pipeline is ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Docker creates a mount point the image does not contain as root, so the volume
mounted at /home/agent/.claude arrived read-only to the agent user and Claude
Code could not write its settings or credentials. /work already avoided this by
being created in the image; the home directory now does the same.

CRYPTER_GIT_NAME and CRYPTER_GIT_EMAIL were read by the clone script but never
passed through containerEnv, so every commit was authored as the fallback
identity. CRYPTER_WORKSPACE could never diverge from workspaceFolder without
breaking the container, so it is now a constant.
Crypter.Test's project graph does not include Crypter.Web or Crypter.Test.Web,
so building it alone left changes to either uncompiled until CI, which builds
the whole solution and runs both test projects.
Checks run on draft pull requests, so the round of checks starts when the pull
request is created at stage 3 and again on every push. `gh pr ready` fired
nothing, because ready_for_review is not among the events unit-test.yml and
codeql-analysis.yml listen for. Stage 7 waited on a run that would never come,
and on a clean run — where triage accepts nothing and no remediation is pushed —
it reported the fork's workflows as disabled and stopped.

Stage 7 now resolves the branch's head commit and watches the round of checks
for it, and the pull request stays a draft for the user to publish once they
have reviewed it. `publisher` is renamed `ci-watcher` to match.

The skill also pushes, at stage 3 and after each implementer stage. Nothing
cancels a superseded round of checks, so a push per commit would spend several
rounds to learn what the last one says.
Both agents have Bash, so "you cannot edit code" was a rule dressed up as
a capability boundary. An agent that finds the claim false has no reason
to treat the rest of the instruction as binding either.

Reviewer findings are checked at triage, in a context that did not write
them. "Verify before you report" read as licence to run code and confirm
its own finding, which is the one check that context cannot perform
honestly. Grounding a finding means reading the code paths, not executing
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the repository said what CRYPTER_FORK or CRYPTER_FORK_TOKEN
were, what scopes the token needs, or that GitHub disables workflows on a
new fork until you enable them by hand. That last one surfaces as the
pipeline reporting no CI run ever appeared, which reads like a bug.

The devcontainer image now takes its owner from the environment, still
defaulting to the org's published image. The build workflow tags from the
repository owner, so a fork that changes the Dockerfile publishes to its
own namespace and previously had no way to point the container at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A broken Dockerfile previously surfaced after merge, when the push
workflow ran on stable, or not until someone rebuilt their container.

The gate is a new output on detect-code-changes rather than an
on.pull_request.paths filter. A workflow filtered out by paths reports no
check at all, which leaves a required check pending forever; a job skipped
by an if condition still reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There are five, not four, and all of them gate on detect-code-changes
rather than only the two image builds. An agent expecting four checks
reads the fifth as something having gone wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`gh pr checks` reports job names, not workflow names, so the previous list
matched nothing the agent would actually see. It also missed
build-and-test-web entirely: unit-test.yml runs two jobs, and that is the
one covering Crypter.Test.Web, which sits outside Crypter.Test's project
graph and so cannot be built inside the container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Omitting `tools` grants everything, including Agent. An implementer that
can spawn its own subagents undercuts the premise the pipeline is built
on, which is that each stage starts from a context that did not write the
work it is looking at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build-and-push-devcontainer.yml filters itself into its own push trigger but
was absent from the pull request gate, so edits to it merged without ever
building the image they publish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
API_IMAGE_NAME and WEB_IMAGE_NAME are variables because deployments vary.
The devcontainer image has one consumer, devcontainer.json, which names it
literally, so the only value DEVCONTAINER_IMAGE_NAME could hold was the one
now in the workflow. Unset it produced a malformed reference; set wrong it
published an image nothing would pull.

Setting up a fork is one step shorter as a result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The setup document covered the fork, the token and enabling Actions, then
stopped short of the one credential the pipeline cannot run without. The
crypter-pipeline-claude volume exists to persist it and nothing said so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The container was described only by devcontainer.json, which needs a separate
tool to interpret it and left the setup documentation with no instruction for
starting anything. Compose is already a dependency of this repository, so the
container can be described the way the application stack is, with its values in
a tracked .env that reads like the root one. devcontainer.json goes rather than
sitting alongside, so there is one definition to keep correct.

It is a separate Compose project rather than a service in the root file. The
agents only ever run dotnet build, git and gh, so they need nothing from the
api, web or db services, and sharing a file would put the container on the
application stack's network and leave a root `compose down` unable to remove a
network still in use.

crypter-clone-fork now runs on every start rather than once at creation. It
already leaves an existing workspace alone and only refetches, so work in
progress still survives a restart.

The image bakes in clone-fork.sh, so its corrected guard messages need an image
rebuild to take effect.
…peline

Add an agentic pipeline that runs in a devcontainer
The publish workflow pushed to GHCR as soon as anything under
.devcontainer/ landed on stable. It now runs in the devcontainer
environment, so a reviewer approves the run before the image moves.

Publishing from a fork is not supported, so its instructions are gone
along with CRYPTER_DEVCONTAINER_OWNER, which existed only to point the
container at a fork's image. Testing an image change is now a local
build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A build section makes Compose build the image when it is absent locally,
without ever contacting the registry, so every first start built the
image from scratch. Declaring the pull policy restores the pull and
leaves `compose build` available for image work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ainer-publish

Gate the devcontainer image publish and pull it instead of building
CRYPTER_FORK_TOKEN is a live GitHub personal access token, and while the file
was tracked the only thing keeping a filled-in token out of a commit was the
contributor noticing what they staged. Git now ignores the file, so it cannot be
committed by accident, and .devcontainer/.env.example carries the four keys to
copy from.

Anyone who already has a filled-in .devcontainer/.env keeps it: the file stays on
disk here, only its tracking stops. Pulling this commit with a modified copy
aborts the merge, and untracking alone is not enough — git then refuses to remove
what has become an untracked file. Untrack it and commit that removal before
pulling:

    git rm --cached .devcontainer/.env
    git commit -m "Untrack devcontainer env"
    git pull

The merge then succeeds, the file and its token survive, and the tree is clean.

The ignore rule is anchored to .devcontainer, so the root .env stays tracked.
A contributor who already has a filled-in .devcontainer/.env hits a
modify/delete conflict on the pull, and git's printed resolution,
git rm .devcontainer/.env, deletes their live token. The doc now gives the
git rm --cached sequence to run before pulling instead.

The same section claimed the file is ignored by git so the token cannot be
committed by accident. That holds only on a fresh checkout: .gitignore does
nothing for an already-tracked path, so any branch cut before this change
still commits the token as before. The claim is now scoped to fresh checkouts.
CRYPTER_GIT_NAME and CRYPTER_GIT_EMAIL fell back to a generic name and a
noreply address, so an unconfigured container pushed commits to a
contributor's fork under an identity belonging to nobody. Both are now
guarded in clone-fork.sh the same way CRYPTER_FORK and GH_TOKEN already
were, and Compose passes them through unsubstituted.

The setup document's notes on moving off the tracked file described a
migration rather than the setup as it stands, so they are gone along with
the qualifications about branches cut before the file stopped being
tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stop tracking .devcontainer/.env and ship a template instead
The plan now comes from /crypter-plan-author, a skill an interactive
session runs on the host, where the web, the user's tooling and the user
are all reachable. Compose mounts .claude/plans read-only at /plans, so
the container reads the plan where it was written and keeps its own run
state in the workspace.

/pipeline takes a run id and a branch and starts at the sync, with the
host session driving it once the plan is approved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The container ran with a fork-scoped PAT and unrestricted egress, since
Claude Code needs the Anthropic API. Rather than harden egress, the
credential is gone: the workspace is now an anonymous clone of the org
repository whose only remote has no push url, so the agents read public
code and commit locally.

Pushing, opening the pull request and the CI loop move to the host, where
the session already has GitHub access. /crypter-publish fetches the branch
out of the container over git's ext transport, pushes it, opens the draft
pull request, and runs at most three fix attempts, handing each failure to
/pipeline-fix through the read-only plans mount.

CRYPTER_FORK and CRYPTER_FORK_TOKEN leave .env, and the image drops the
GitHub CLI, which has nothing left to authenticate with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One skill drafted a plan and launched a container, and another implemented,
reviewed, triaged and remediated. Neither name described what it did, and
neither part could be used alone.

The work now sits in small skills that compose: crypter-plan, crypter-implement,
crypter-examine, crypter-remediate and crypter-publish. crypter-change carries a
requirement through all of them and owns the CI loop, where the plan, the
findings and every previous attempt are already in context. crypter-review runs
the same lenses against a pull request that already exists, including one raised
by someone else, and reports without posting anything.

Findings become artifacts on the host. A second mount carries .claude/runs into
the container, and each reviewing agent writes its own file there, so what was
found and what was rejected can be read without docker exec. The orchestrators
create those directories because the container's agent is uid 1001 and host
files are uid 1000.

The reviewer's lens table grows from one row to correctness, maintainability,
testability and security.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
crypter-examine claimed the branch it reviewed, so it failed whenever anything
else already held that ref. It only reads, and a detached worktree reads just as
well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pipeline reviewed its own branch. Acting on what it raised:

crypter-publish becomes crypter-open-pull-request, since the skill only
ever opens a draft and taking a pull request out of draft is the user's
call.

ci-watcher loses the fork parameter it was never given and resolves the
repository from the path it already has, and reads the branch's version
of a file rather than whatever the working tree is checked out at.

crypter-review forces its fetch refspec, so a pull request that was
rebased or amended can be reviewed again.

The mount table named the run-id directory as the mount source; the
mounts are the parents. The pre-flight check tested the parent of /runs,
which uid 1001 cannot write to by design, so it now tests the run
directory the caller just created. The launch steps create both mount
sources, because Docker creates a missing bind-mount source as root.

The documentation credited a fork-scoped token with bounding the
container and claimed it holds no credential; the Claude Code credential
is still there, so it says GitHub credential and names the container as
a trust boundary. The orchestrators state that they run from the main
checkout, which is where the mounts resolve.
The agents run unattended, which is the whole reason permissions had to
be resolved without a prompt. Auto mode does that without handing them
the bypass, and the unprivileged user in the image stands on its own
merits rather than on what the bypass refuses to do as root.
n and others added 15 commits August 5, 2026 11:14
.claude/runs is a writable mount into the pipeline container, and
/crypter-review runs agents over diffs written by people outside this
project. A symlink left in that directory resolves on the host side, so
reading a findings file can read any file the user can, and writing
triage.md can overwrite one.

The hook blocks a path inside the project that resolves outside it.
Paths that point outside to begin with are untouched, since asking for
one is deliberate. It is written in Node because the build already
depends on it, so it runs where a shell script would not.
A review that lands only on the reviewer's disk asks the reviewer to
retype it. crypter-review now triages what the lenses raised and posts a
single review that comments, with a line comment for each finding that
lands inside the diff and the rest in the body.

It never approves and never requests changes. The pull request may
belong to someone else, and either verdict is the user's to give.

Triage comes first, and it is written to triage.md, so the findings that
reach the author are the ones that survived a read against the code, and
so a later remediation run has something to start from.
A finding on a pull request is a claim, and acting on a wrong one means
changing working code. crypter-triage-review gives each finding its own
verifier with the code and nothing else to judge it by, so no agent ever
rules on a finding it raised.

What holds becomes a commit. What does not gets a reply on its thread
carrying the evidence, so the person who raised it can answer. What the
verifier cannot settle goes to the user untouched.

The commits go back to the pull request's own branch, which is only
possible when it comes from a repository the session can push to.
Otherwise the replies stand on their own and the author does the fixing.
The skills are visible wherever the repository is checked out, and four
of them only work inside the pipeline container, where /work/Crypter,
/plans and /runs exist. The prefix says which is which, so the skills a
user invokes are the ones without it.
The two skills a session runs on behalf of an orchestrator now say so in
their names. crypter-step- rather than crypter-change-step- because
open-pull-request is a step of crypter-triage-review as well.

What is left unprefixed is what a user invokes: crypter-change,
crypter-review, crypter-triage-review.
Restructure the agentic pipeline into task skills and orchestrators
The pipeline skills assumed origin was a personal fork and upstream the org
repository. This checkout has one remote, origin, pointing at the org, so
crypter-step-plan failed on its first command and the push step had no
upstream/stable to sync a fork from.

Branches now go to the org repository the way every branch before the pipeline
did, which removes the fork sync push and the second pull request that had to
be opened by hand afterwards. crypter-step-open-pull-request checks origin
before pushing, since a checkout wired up differently sends the branch
somewhere the pull request will not find it.

Container-side skills keep the upstream remote name. That is the name the
container's own clone gives its single read-only remote, and it is unaffected.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Watch CI with gh instead of a polling loop

The CI watcher was told to poll for check runs every thirty seconds. A
foreground sleep does not run in this harness and the agent has no timer, so
the interval it was asked for was one it could not take. The documented way out
was gh pr checks --watch, and gh was not installed, so that path was dead too.

gh is now a hard dependency rather than an alternative offered where it happens
to exist. Watching is a single blocking --watch call, and the exit code
distinguishes a failure from a watch cut short before the round finished, which
would otherwise burn a fix attempt on a run that was merely still going.

Failure diagnosis reads the failing job's log through gh run view --log-failed
rather than scraping check run annotations. Annotations carry compiler
diagnostics well and test failures poorly, and the report this agent writes is
the only thing the implementer gets.

gh carries its own credential, separate from the MCP server's, so the agent
checks gh auth status first and names the missing login rather than failing
part way through with an authentication error.

crypter-triage-review named gh pr comment as the only way to answer a finding
with no thread, which was unreachable for the same reason. It now names the
MCP equivalent alongside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Treat a gh authentication failure as a setup problem

An unauthenticated gh pr checks exits 4, which was outside the set of exit
codes the watcher was given. Left undocumented it reads as a non-zero exit and
therefore as a failing build, which sends the implementer looking for a defect
that is not there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Move the CI watcher's mechanics into a script

Waiting, reading exit codes, finding failing runs and cutting a log down to the
part that explains itself are all deterministic, and they were written as prose
for an agent to follow. Prose cannot be run, so the mistakes in it only surfaced
when a run went wrong: a polling interval the harness cannot take, an exit code
that reads as a failing build when it means nobody logged in, an instruction to
read the tail of a log whose last fifty lines are cleanup.

ci-status.sh does that half and can be tested. Exit codes separate a CI failure
from the three setup problems that otherwise look like one. On a failure it
prints the window of log ending at the runner's ##[error] marker, which turns a
1100-line dump into roughly sixty lines containing the cause.

The agent keeps the half that needs judgement: reading back from the symptom to
the cause, reading the branch's version of the code the failure names, deciding
whether it is a defect, a wrong test or the plan itself being wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Keep gh behind the script rather than in agent prose

Two skills offered a bare gh call as an alternative to the MCP server for
posting a review and commenting on a pull request. The MCP server does both, so
those were discretionary CLI invocations with nothing to recommend them.

The watcher's remaining direct call was fetching a fuller copy of a log the
script had already downloaded. The script now keeps each failing job's log and
prints its path, so reading further means opening a file rather than going back
to the network. Nothing outside ci-status.sh invokes gh now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Build and review in per-run container workspaces

The container kept a long-lived clone of the org repository in a named volume.
Nothing ever advanced its working tree, so the pipeline's own skills and agents
were frozen at whatever commit the volume was created with. The rename in #842
made that visible: the host began invoking crypter-devcontainer-implement while
the container still only knew crypter-implement, and both orchestrators broke.

Workspaces are now created per run at /work/{run-id} and deleted when the run
ends, cloned from a read-only mount of the host's .git. A copy of the code no
longer outlives the run that made it, so it cannot drift. The mount being
read-only is what makes sharing history safe: the container reads commits and
cannot move a ref or add an object. The host working tree is not mounted, so
uncommitted work stays invisible to the agents.

The orchestrator owns the lifecycle and the container skills assume a workspace
exists, mirroring how /runs already works. This also retires the worktree
add/remove pairing in every container skill, along with the stale worktree a
crashed run used to leave behind.

The image is built locally instead of pulled from GHCR. It carries tooling and
no source, so a change to workspace.sh or the Dockerfile is a rebuild rather
than a publish waiting on an approval.

NuGet and pnpm caches move to named volumes. Workspaces are ephemeral, so
without them every run would restore from nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop publishing the devcontainer image

The image is built locally and carries tooling rather than source, so nothing
consumes the published copy. Publishing it on every merge to stable left a
registry image nobody pulled and an approval gate on changes that only ever
affect the machine making them.

pr-build-devcontainer stays. It pushes nothing and catches a Dockerfile that
does not build, which is the only part of the publish path that was earning its
keep.

The devcontainer environment on the repository has no jobs left referencing it
and can be deleted from the repository settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The skills open at `docker exec crypter-pipeline` and assume the container is
running and belongs to this checkout. Neither holds by itself. The Compose
mounts are relative paths, so they resolve against whichever checkout launched
the container, and a second checkout finds a container by name whose /runs
writes land somewhere it never looks and whose /host-git is a different history.

The trap is that an exited container looks repairable. `docker start` reuses the
mounts and image fixed at creation, so it brings the wrong container back and the
run fails two steps later at workspace creation, reading as a missing executable
rather than as the wrong container.

crypter-review and crypter-triage-review gain the preflight crypter-change
already had, and all three now also check the image carries crypter-workspace,
which the mount checks alone let through. The checks are `test` rather than
`command -v` because docker exec runs a binary and not a shell, so a builtin
exits 127 either way.

The remedy is `up -d --build`, not `--force-recreate`, which recreates against
new mounts but keeps a stale image. It takes over the one crypter-pipeline on
the machine, so the skills ask before running it.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
crypter-review reviewed every pull request against stable. A release pull
request targets main, so the diff it handed the lenses was stable against
itself: empty. Four lenses read nothing and reported nothing wrong, which is
indistinguishable from a clean review.

The skill now takes the base branch from the pull request and passes it down,
and crypter-workspace takes it as --base, defaulting to stable for work built
here. A base that does not resolve is caught before the clone, so a corrected
retry is not refused by the half-built workspace of the failed one.

The workspace's second name for the org repository goes with it. A clone maps
the source's local branches into origin/*, which is what upstream/stable existed
to work around; pointing remote.origin.fetch at the host's remote-tracking refs
gets the same guarantee under one name, and keeps it against a later bare fetch.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Every lens was handed the same list of Crypter conventions while also being
told to stay inside its lens, so the security lens was primed to report a
missing Async suffix and forbidden from reporting it in the same breath. Which
way a reviewer resolved that was left to chance.

The conventions are now split by what goes wrong when they are broken, and each
half lives in the lens brief that can judge the consequence: monads, sync IO,
constructors, enums and missing migrations under correctness; history-narrating
comments and Async naming under maintainability; validated primitives under
security. Testability claims none, rather than being given one to justify the
symmetry.

That moves them out of the shared agent definition and into the prompt, where
the skill already said the lens belongs. The briefs must now be passed in full
for the conventions to be reviewed at all.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The host skill read every lens finding against the code to decide what to
post, which put a judgement about code in the session that has no lens
discipline and no worktree. In practice it drifted further than that: with
an examine run that produced nothing, reading the diff and forming findings
directly looked like the reasonable way to fill the gap.

Findings now go to /crypter-devcontainer-verify, which already rules on
findings from anywhere else and already rejects preferences and findings
about untouched code. It grows a second input shape for this: given the
lenses' own directory it assigns ids per lens rather than expecting a
collected report, so the host hands over a path and reads nothing until
verdicts exist.

Only findings that hold reach the pull request. One that a verifier ruled
against is the pipeline checking itself, and it stays in .claude/runs where
a later pass can read it, rather than costing the author a read to reach a
conclusion the verifier already reached.

Both container calls exit 0 whichever way they go, so each step now checks
what was written instead of trusting the status, and distinguishes an empty
result that means success from one that means the run never happened.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#852)

The pipeline container was welded to one checkout. Its mounts are relative
paths resolved at creation, and both the container and Compose project names
were hardcoded, so a second checkout running the pipeline reached the first
one's container: findings written under a repository nobody was looking at,
and workspaces cloned from a history nobody was reviewing.

Workspaces now clone from CRYPTER_REPO_URL rather than from a read-only mount
of the host's .git. A run sees the branch as the repository holds it, so the
launching checkout no longer decides what gets built or reviewed, and a stale
or unfetched host is no longer a way to start a run on the wrong base. Pull
request heads come from refs/pull/{n}/head directly, which removes the
host-side fetch that staged them. The origin/* fixup in workspace.sh goes with
it: it existed only because cloning a checkout maps that checkout's local
branches into origin/*, which cloning the real remote does not.

Dropping the mount also makes a git worktree a usable checkout for the
pipeline. Its .git is a file rather than a directory, so mounting it gave
git clone something it could not read.

Containers are now named crypter-pipeline-{slug}-{hash}, derived by
pipeline.sh from the checkout it sits in. A checkout resolves only to its own
name, so which container an orchestrator reaches is settled by where it runs
rather than by probing mounts, and recreating one leaves the others alone.
The three volumes stay shared machine-wide, so Claude Code is still
authenticated once and the caches warmed once; they are external because a
Compose-owned volume refuses to mount into a second project.

Compose requires the container name rather than defaulting it, so invoking
docker compose against the file directly now fails instead of quietly
creating a container whose identity says nothing about its mounts.

The container needs outbound network for source as well as for the API, so
the pipeline no longer works offline. It clones anonymously over https and
holds no GitHub credential; branches still leave over the ext:: transport and
are pushed from the host.

Co-authored-by: n <e@f>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude Code in the container authenticates from an interactive login held in
the crypter-pipeline-claude volume, shared by every checkout on the machine.
That login lapses on its own schedule, and renewing it meant knowing to open a
shell in the container and run `claude` there.

`pipeline.sh login` makes that a command. CLAUDE_CODE_OAUTH_TOKEN is an
alternative for a machine that would rather configure the credential than open
a browser: Compose reads it into the container's environment, where Claude Code
uses it in place of the stored login. Replacing an expired token means editing
.env and running `up` again, since the environment is fixed when the container
is created, so the stored login remains the better default.

`up` warns when neither is set rather than refusing, because either one on its
own is enough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Jack-Edwards
Jack-Edwards force-pushed the chore/pipeline-oauth-token branch from b7587cc to 40c6059 Compare August 7, 2026 00:50
@Jack-Edwards Jack-Edwards changed the title Authenticate the pipeline container with an OAuth token Authenticate the pipeline container by login or by token Aug 7, 2026
@Jack-Edwards

Copy link
Copy Markdown
Member Author

Closing unmerged. The development pipeline now lives in Crypter-File-Transfer/Claude-Development-Pipeline, and this branch's changes went across with it — .devcontainer/.env.example, .devcontainer/docker-compose.yml, .devcontainer/pipeline.sh and the pipeline document are byte-identical there.

stable no longer carries the pipeline, so there is nothing here left to merge into.

@Jack-Edwards
Jack-Edwards deleted the chore/pipeline-oauth-token branch August 7, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant