Skip to content

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).

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:

  1. 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.

  2. 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.

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 ceiling

If runs routinely approach the limit, also consider narrowing the request (see above) or a faster model for the mode.

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_files now refuses that before anything is read, so this shows up as a refused commit.

What to change:

  1. Narrow the request, or split a large PR into smaller ones.
  2. Turn off context.full_diff if it’s on: a compressed diff uses far less of the budget this is protecting.

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 MB

And 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 MB

The tracking comment says “ran out of memory” instead of the generic crash wording whenever that report is there.

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.

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.

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).

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.

pnpm install / npm install can’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.

the reply ends with I did not commit anything…, or a commit fails with Resource not accessible by integration

Four reasons, in order of likelihood:

  • The mention didn’t ask for a change. mention mode 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, or modes.implement.enabled: false, which turns writes off everywhere unless you set permissions.write: true. See permissions.
  • 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 from POST /repos/…/git/blobs at the very end of a run.

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:

  1. Check what ran. The message lists the first few paths. If they’re unrelated to the request, a repo-wide command is the cause.

  2. Raise the ceiling if a change this size is genuinely intended:

    limits:
    max_commit_files: 1000 # default 200, or 0 for no ceiling

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:

  1. Add the path to .gitignore, or write the credential outside the checkout.
  2. 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.
  3. Commit it yourself if it genuinely belongs in the repository.

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: false if you’d rather not pay for it, or retry the round once the runner is less busy.
  • gitleaks is missing (ENOENT). It isn’t on PATH in 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.

a review was submitted on a crab’d pull request and nothing happened

  • You are on Forgejo. Forgejo Actions has no pull_request_review or pull_request_review_comment trigger, so a review cannot start a run there at all. Comment /crabd implement address the review on the pull request instead.
  • The workflow doesn’t subscribe to the event. Add pull_request_review: types: [submitted] and pull_request_review_comment: types: [created] to the on: block. The template in workflows/github/crabd.yml has 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 because implement itself 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.

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.

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.

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.

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 skipped means its if: 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 the github.event_name caveat.
  • The event isn’t one crab’d handles. The logs end with event "..." is not handled. Skipping. crab’d acts on pull_request, issue_comment, pull_request_review, pull_request_review_comment, and issues only. A push, for example, is a deliberate no-op.
  • No trigger matched. A comment must contain the trigger phrase, and a pull_request only auto-reviews on opened / reopened / ready_for_review, not on a push to the branch. Mention /crabd review to re-review.
  • The pull request is a draft. Draft PRs are never auto-reviewed. Mark the PR ready for review, or mention /crabd review in 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.