Troubleshooting
When a run fails, crab’d updates its tracking comment with what happened, what to change, and a link back here — instead of a raw stack trace. This page expands on each failure it can post.
Every failure comment names the specific config knob to adjust. The knobs live under
limits and rate_limit
in your .crabd.yml (or the equivalent in
crabd.config.ts).
Run hit the turn limit
Section titled “Run hit the turn limit”crab’d stopped … — it reached the tool-call limit before finishing.
Every run has a hard ceiling on tool-calling turns (limits.max_turns, default 40). When a
run reaches it, crab’d is stopped mid-task. This is a safety valve against runaway loops and
unbounded CI cost. The model is told the approximate figure up front so it can spend it sensibly,
and submit never counts against it, so a finished answer is never the call that gets cut off.
Before it stops, crab’d reserves a few turns to ask the model for a best-effort final answer — so the comment usually carries a partial result (what it found, what’s still open) rather than nothing. When it does, the comment is marked as partial.
Common causes:
- The task was too broad for one run — e.g. “review this” on a PR touching dozens of files, or an open-ended “look into X”.
- crab’d chased something it couldn’t finish — most often a file or another repository it has no access to (its token is scoped to the current repo), then looped retrying.
What to change:
-
Narrow the request. Point crab’d at the specific files or the specific change you want feedback on, or split a large PR into smaller ones.
-
Raise the ceiling only if the task genuinely needs more steps:
limits:max_turns: 80 # default 40
Raising max_turns trades CI minutes (and tokens) for headroom — prefer narrowing the task first.
Run timed out
Section titled “Run timed out”crab’d ran out of time … — the run exceeded its time limit.
Every run has a wall-clock ceiling (limits.timeout_minutes, default 20). It’s one deadline for
the whole run: a rate-limit retry and a fallback-model switch spend the same budget, and neither
gets a fresh one.
Shortly before the deadline crab’d stops exploring and asks for a best-effort final answer, the same reserve the turn ceiling keeps, so a run that runs out of time still reports what it has.
A single shell command has its own ceiling (limits.command_seconds, default 300). A command
that passes it is killed and the model gets exit code 124 with a message naming the limit, which
keeps one slow build from eating the whole run.
What to change:
limits: timeout_minutes: 30 # default 20, or 0 for no ceilingIf runs routinely approach the limit, also consider narrowing the request (see above) or a faster model for the mode.
Run ran out of memory
Section titled “Run ran out of memory”crab’d ran out of memory while … Its heap usage hit the safety limit before it could finish.
A watchdog samples crab’d’s own process heap while a turn runs. If usage climbs past a safety
threshold, crab’d aborts the turn itself and reports it here, rather than letting the process
hard-crash, which would leave the tracking comment stuck on “working…” with no explanation. This is
a safety valve, not a user-configurable limit: there’s no limits.* knob for it, the same way there
isn’t one for the turn or timeout ceilings’ underlying mechanism.
Common causes:
- An unusually large diff or file was pulled into the prompt (see
context.full_diff). - A long-running conversation kept growing turn over turn without producing an answer.
- A working tree far bigger than the change, where a repo-wide formatter or a dependency
install rewrote thousands of files and the commit path read every one of them.
limits.max_commit_filesnow refuses that before anything is read, so this shows up as a refused commit.
What to change:
- Narrow the request, or split a large PR into smaller ones.
- Turn off
context.full_diffif it’s on: a compressed diff uses far less of the budget this is protecting.
Reading the log after a heap crash
Section titled “Reading the log after a heap crash”The watchdog samples on the event loop, so a heap that climbs to the ceiling inside a single synchronous step outruns it and takes the process down with no warning line. Two things in the log cover that case.
A reporter on its own thread keeps writing while the main thread is stuck, so you get a line naming the memory in use and the work that was running:
[crabd] the main thread has not responded for 43s during tool bash pnpm build, rss 5212 MBAnd node writes a diagnostic report on the way down, which the post step reads and prints:
[crabd] the run aborted: OOMError: Allocation failed - JavaScript heap out of memory[crabd] heap 6104 MB of 6144 MBThe tracking comment says “ran out of memory” instead of the generic crash wording whenever that report is there.
Run crashed or was cancelled
Section titled “Run crashed or was cancelled”crab’d stopped unexpectedly while … The run ended before it could finish or report a result.
The action’s post step runs after a crash and after a cancel, and it does two things: it rewrites the tracking comment, and it leaves a short comment of its own. The second one exists because editing a comment notifies nobody. It replies in the thread when an inline review comment set the run off, and posts on the pull request otherwise, addressed to whoever triggered the run.
What to change: read the run logs first, since they’re the only place detail on a crash like this survives. Then apply the same fix as a turn-limit or memory failure: narrow the request, or split a large pull request.
Rate limited
Section titled “Rate limited”crab’d … every model was rate-limited …
The provider rate-limited or overloaded crab’d, and every model in the fallback chain was exhausted (or the wait budget ran out). This has its own tuning surface — backoff, an ordered fallback-model chain, a wait budget, and per-mode soft/fail behavior. See Rate limiting & fallback models.
Configuration is invalid
Section titled “Configuration is invalid”crab’d couldn’t start … — its configuration is invalid.
crab’d couldn’t load or validate your config. The details block names the offending field. Check it
against the .crabd.yml reference or
crabd.config.ts reference, and see
Config layering & governance if a value isn’t taking effect (a higher layer may
override or forbid it).
Cross-repo reads aren’t working
Section titled “Cross-repo reads aren’t working”the agent can’t read another repository you granted with
repos.read
Most often the token can’t reach it. repos.read needs a cross-repo-capable token: your own App
(CRABD_APP_*, installed on those repos) or a scoped PAT. Under the default token broker — which
vends single-repo tokens by design — repos.read is ignored (the run logs this). Check the run logs,
and see Cross-repo access & private registries.
Private registry install fails
Section titled “Private registry install fails”
pnpm install/npm installcan’t authenticate
Confirm the secret is mapped to an env var on the crab’d step (env: { NPM_TOKEN: ${{ secrets.… }} }), that the same name is what the sandbox.npmrc entry’s token_env references (or is listed
in sandbox.env for a non-registry secret), and that the registry URL/scope match your dependency. A
name mismatch between the step’s env, token_env, and the secret is the most common cause. See
Cross-repo access & private registries.
crab’d logs a warning when it can’t resolve a registry token —
sandbox.npmrc: token env "NPM_TOKEN" is not set … — and tells the agent that registry is
unauthenticated so it reviews from source instead of thrashing on install retries (a missing token
otherwise burns a whole run’s tool budget on 401/403s). If you see that warning, the token never
reached the step. Note the forge-token fallback (omitting token_env for same-org GitHub Packages)
needs your own GitHub App with packages: read — it does not work under the hosted broker; use an
explicit token_env there.
crab’d won’t commit
Section titled “crab’d won’t commit”the reply ends with
I did not commit anything…, or a commit fails withResource not accessible by integration
Four reasons, in order of likelihood:
- The mention didn’t ask for a change.
mentionmode commits only when the comment asks for one. A bare/crabd, or a question, gets an answer even if crab’d noticed a fix worth making. Ask for the change explicitly. - The pull request comes from a fork. Its branch lives in another repository, which crab’d cannot write to. The reply names the repository and lists the files it would have changed.
- Writes are off. Either
permissions.write: false, ormodes.implement.enabled: false, which turns writes off everywhere unless you setpermissions.write: true. Seepermissions. - The token can’t write. Your GitHub App installation grants
contents: read. crab’d now detects this at startup, warns, and runs read-only instead of failing at the commit, and the run log carries the warning. Grant the App Contents: Read and write, then accept the permission request on the installation (an org owner has to approve it; raising the App’s permissions alone does nothing until the installation accepts). Older versions surfaced this only as a 403 fromPOST /repos/…/git/blobsat the very end of a run.
The commit was too wide
Section titled “The commit was too wide”
refusing to commit <n> changed files, over the limit of 200
A commit is capped at limits.max_commit_files files. The count
is taken from git status before any file is read, so an accidentally enormous working tree costs
nothing to reject.
A change this wide almost always comes from a command that rewrites the whole repository: a
repo-wide formatter, a linter run with --fix, pnpm install, or pnpm dedupe. crab’d commits
every file whose contents changed during the run, so one of those buries the change you asked for
under thousands of unrelated files, and reaches anything else the workflow left in the checkout.
The prompt names those commands and tells the agent to format by path.
Untracked build output is skipped, so a new dist/ or store directory a test run leaves behind
never reaches the commit and never needs a .gitignore entry.
What to change:
-
Check what ran. The message lists the first few paths. If they’re unrelated to the request, a repo-wide command is the cause.
-
Raise the ceiling if a change this size is genuinely intended:
limits:max_commit_files: 1000 # default 200, or 0 for no ceiling
The commit carried a credential
Section titled “The commit carried a credential”
refusing to commit \.env`. That path looks like a credential, and it is not tracked in this repository.`
crab’d checks the paths in a commit before it reads any of them, independently of the gitleaks scan
below. An untracked path shaped like a credential (.env, *.pem, *.key, id_rsa, a
service-account*.json, kubeconfig, and similar) refuses the whole commit.
Only untracked paths are checked. A repository that already tracks test/fixtures/key.pem has made
that decision, and a run editing it commits normally. .env.example and the other .example /
.sample / .template suffixes are exempt.
The usual cause is a workflow step that writes a credential into the checkout.
google-github-actions/auth puts its ADC file in $GITHUB_WORKSPACE, and
aws-actions/configure-aws-credentials can be pointed there too. crab’d drops the files it
recognises as generated (gha-creds-*.json, .aws/credentials,
application_default_credentials.json) from every commit, whatever the baseline says about them,
and logs the drop.
What to change:
- Add the path to
.gitignore, or write the credential outside the checkout. - Check why it changed. A pre-existing file is normally invisible to the commit, because crab’d
only commits what changed during the run. A repo-wide formatter reaches these files too: one run
ran
prettier --write ., which reformatted the workspace’s ADC file and made it look like the run’s own work. - Commit it yourself if it genuinely belongs in the repository.
The secret scanner could not run
Section titled “The secret scanner could not run”
refusing to commit, the secret scanner did not finish scanning <n> files within <timeout>ms, even after a retry
crab’d scans every commit and memory write for secrets with gitleaks before it reaches the forge
(permissions.secret_scan, on by default), and refuses the
write if the scan can’t produce a verdict. The check is deliberately fail-closed: an unscanned
commit never goes out.
Three shapes:
- The scan timed out, even after a retry. The message names the timeout and how many files
were in flight. A large change set on a loaded runner can outrun the default 120-second budget.
Turn the scan off on purpose with
permissions.secret_scan: falseif you’d rather not pay for it, or retry the round once the runner is less busy. - gitleaks is missing (
ENOENT). It isn’t onPATHin the image crab’d is running in. That’s a crab’d packaging problem, not your config, so please open an issue. - Anything else. The message carries what gitleaks or the OS reported. Check the run log and, if it looks like a bug in crab’d, open an issue.
No feedback round started
Section titled “No feedback round started”a review was submitted on a crab’d pull request and nothing happened
- You are on Forgejo. Forgejo Actions has no
pull_request_revieworpull_request_review_commenttrigger, so a review cannot start a run there at all. Comment/crabd implement address the reviewon the pull request instead. - The workflow doesn’t subscribe to the event. Add
pull_request_review: types: [submitted]andpull_request_review_comment: types: [created]to theon:block. The template inworkflows/github/crabd.ymlhas both. - crab’d doesn’t own the pull request. Automatic rounds only fire on pull requests crab’d
opened, recognized by a hidden marker in the description or by the
crabd/branch prefix. On anyone else’s pull request, ask for the round with/crabd implement address the review. - The review had nothing to act on. An approval with an empty body, and a comment-only review with no body and no inline comments, are both no-ops.
- Rounds are off, through
implement.rounds.enabled: false, or becauseimplementitself is disabled, which also turns writes off. - Another run got there first. A submitted review with inline comments arrives as several
events at once. The first run claims the round and the rest skip it, which the log records as
feedback round for <sha> was already handled. That is the intended behavior: one round answers everything that is open.
A round replied but resolved nothing
Section titled “A round replied but resolved nothing”crab’d resolves only the conversations it marked as fixed, and only on GitHub. The Forgejo API has no endpoint for resolving a conversation in any version through v16, so a Forgejo round posts one summary comment and leaves the resolving to you. It says so in the comment.
A round refused because the branch moved
Section titled “A round refused because the branch moved”
refusing to commit: <branch> moved from abc12345 to def67890 during this run
Something else pushed to the pull request while crab’d was working. crab’d commits whole files read from its checkout, so applying them on top of a newer commit would revert whatever landed in between. Nothing was committed. Ask for the round again and it works from the new head.
A concurrency: block in the workflow prevents the version of this that crab’d causes itself, where
two of its own runs commit to one branch. Both workflow templates ship with one.
Unexpected error
Section titled “Unexpected error”crab’d hit an error …
A failure that doesn’t fall into the categories above. The comment includes a collapsed Error details block and a link to the run logs — start there. If it looks like a bug in crab’d, please open an issue.
Nothing happened at all
Section titled “Nothing happened at all”No comment, no 👀 reaction, no failure message. crab’d posts a comment for every failure it can attribute, so silence means it never got far enough to have anything to say. In order of likelihood:
- The job never ran. Check the run in your forge’s Actions tab. A job showing
skippedmeans itsif:was false, not that crab’d declined. Note that in a Forgejo reusable workflow the parent job still reports success when the inner job skips, so the run looks green. See thegithub.event_namecaveat. - The event isn’t one crab’d handles. The logs end with
event "..." is not handled. Skipping.crab’d acts onpull_request,issue_comment,pull_request_review,pull_request_review_comment, andissuesonly. Apush, for example, is a deliberate no-op. - No trigger matched. A comment must contain the trigger phrase, and a
pull_requestonly auto-reviews onopened/reopened/ready_for_review, not on a push to the branch. Mention/crabd reviewto re-review. - The pull request is a draft. Draft PRs are never auto-reviewed. Mark the PR ready for review,
or mention
/crabd reviewin it. - The actor was filtered. Bots never trigger crab’d (to avoid comment loops), and the actor’s
association must be in
permissions.allowed_associations.
