The previous commit pushed handle-issue's prompt to 21587 characters and
GitHub stopped parsing the file: "(Line: 39, Col: 19): Exceeded max
expression length 21000". Because the prompt interpolates ${{ }}, GitHub
treats the whole block scalar as a single expression, and the cap applies
per expression. The failure mode is quiet and total - no job fails,
the workflow itself disappears, its registered name reverts from "Claude
Bot" to the file path, and the only signal is a run attributed to the
push with no jobs in it.
Drop the hand-written stack description, repository map and runtime-fact
list from that prompt and point at CLAUDE.md and docs/architecture.md
instead. Both are maintained, both are already in the checkout, and the
copy in the prompt had drifted from them anyway - it still described the
mtg worker, omitted internal/tunnelmonitor/ and memory.high, and filed
internal/web/runtime/ under "wiring". Only the support-facing facts that
live in neither file are kept: the install one-liner, the random initial
credentials, the distro-dependent env file, the Docker image and the
capabilities fail2ban needs.
handle-issue is now 15069 characters, and a header comment records the
limit so the next edit does not rediscover it in production.
Three problems, all in .github/workflows/claude-bot.yml.
It was silently dead. No comment had been posted since 2026-07-20 while
every run reported success: roughly twenty issues and pull requests each
burned 18-56 turns and up to $2.59, ended with permission denials, and
published nothing. Comment bodies are markdown, markdown is full of
backticks, and inside a quoted `--body "..."` backticks are command
substitution, so the write was rejected and a failed triage looked
exactly like a clean one. The body now goes to /tmp through Write and out
through --body-file, in every branch of both jobs, and each job re-reads
the thread afterwards so a rejected write fails loudly instead of
reporting success. The run transcript is kept as an artifact.
It could reach much further than it claimed. Both jobs that any GitHub
user can trigger declared themselves READ-ONLY in prose while holding
Bash(gh:*), which is not a GitHub-scoped allowlist: `gh alias set --shell`
runs its argument through sh -c and `gh extension install` fetches and
executes code, both as single commands whose first token is gh. That is a
general shell on a runner holding CLAUDE_CODE_OAUTH_TOKEN, which does not
expire with the job. `gh api` accepted any method, issues: write is
repo-scoped rather than issue-scoped, and `gh pr review --approve`,
`gh pr close` and `gh pr checkout` were forbidden in prose only. Those two
jobs now list the subcommands they actually run. The untrusted title and
body are fenced in tags carrying github.run_id, unguessable at the time
the issue is written, and the invariants an allowlist cannot express -
one issue number, labels and title only, /tmp as the sole writable path,
never $GITHUB_ENV - are stated explicitly. Both checkouts get
persist-credentials: false. handle-pr-fix and mention keep their
wildcards: only owners, members and collaborators can trigger them, and
narrowing the maintainer's own path risks more than it protects.
Its review hid findings and its triage quoted stale facts. "Prefer a few
high-signal findings over many low-value ones" is read literally by
Opus - it finds the bug, judges it below the stated bar and says
nothing - while the Severity and Confidence tiers already existed to do
that filtering. The review also never said that the working directory is
the base revision, so it could assert that a case was unhandled in code
the pull request had already rewritten, and label it confirmed, on an
outside contributor's first patch. Four CLAUDE.md conventions were
missing, each a guaranteed miss: openapigen's StructAllow allowlist, the
layering rules including the runtime.Runtime dispatch requirement that
silently breaks multi-node when bypassed, the assertion standard, and
golden share-link fixtures regenerated to turn a red test green. On the
triage side the invalid and duplicate branches were gated three times
over and so never fired, leaving spam to collect a full investigation and
a courteous reply; /etc/default/x-ui was given as the env file when it is
distro-dependent, making the PostgreSQL migration advice a silent no-op
on RHEL and Arch; an env list labelled "full" omitted XUI_PORT and the
XUI_TUNNEL_HEALTH_* family; XTLS was offered as a security option the
panel does not have. docs/architecture.md was invisible to both prompts
despite being maintained and already in the checkout. From the bot's own
output: it published a trigger only the maintainer can use, retitled
issues without saying so, asked for screenshots it cannot open, and once
invented a reason for a number it had miscounted.
All four jobs move to Opus 5, at xhigh effort rather than max - the
recommended tier for agentic work, and one below the overthinking that
max invites on routine triage.
Rework the handle-pr-review, handle-pr-fix, and handle-issue prompts to produce professional, structured output. The review job now rates findings by severity and confidence across explicit review areas and reports a Summary, Findings, and a text-only verdict in one plain comment; the fix job reuses the same lens to prioritize what it applies versus leaves for the author; issue triage gains a structured bug-confirmation format and explicit outcomes for mislabeled and not-a-bug reports, closing conservatively. Severity uses text labels to respect the no-emoji house style, and the adapted ignore-list keeps i18n and generated files flaggable.
claude-code-action only pushes a branch and posts a Create PR link by design; it never opens the PR itself. Add a post-step to the mention job that opens a PR from the action's branch_name output when the trigger was an issue (guarded against no-op branches and against an existing PR).
Simplify the mention prompt so the agent just makes edits with Edit/Write and lets the workflow commit and open the PR, instead of running git/gh pr create itself (which fought the action's built-in flow and left only a link).
Make every automatic, untrusted trigger read-only and require an explicit trusted actor for any code change.
- handle-issue (issue opened): read-only triage; confirm bugs and tag the maintainer, never edit code or open a PR. Authenticates as GITHUB_TOKEN so replies post as github-actions[bot], not a personal account.
- handle-pr-fix (PR opened): applies fixes only for owner/member/collaborator authors; dropped allowed_non_write_users so the default write gate also applies.
- handle-pr-review (PR opened, external authors): read-only review comment only.
- mention (@claude comment): runs only for the repository owner; may open a PR from an issue or commit to a PR on explicit request.
No job authenticates as the static PAT anymore; the PAT is used only to route git pushes for the trusted PR-fix and owner-mention paths.
Split the review-only handle-pr job into handle-pr-fix (owner/member/collaborator PRs: apply refactors and bug fixes directly, commit to the PR branch, no suggestion blocks) and handle-pr-review (external/fork PRs: one review-only comment, no suggestions, no code checkout).
Upgrade handle-issue to open a fix PR for easy bugs (pushed via CLAUDE_BOT_PAT so pull_request CI runs on it), confirm the root cause and tag the maintainer for big bugs, and never open a PR for feature or enhancement requests.
- handle-issue: use Sonnet 4.6 and raise max-turns 150 to 250
- handle-pr: use Opus 4.8; rewrite review as inline comments stating the problem plus a suggestion block, posted as one COMMENT review
- mention: use Opus 4.8; on issues do research only (never commit) with full comment/history context and feature-request feasibility analysis; PR commit-on-request behavior unchanged
- reformat the mention append-system-prompt into a readable multi-line block (verified it still parses as a single CLI argument)
claude-code-action checks out the PR head branch and pushes Claude's
commits with `git push origin ...`. For PRs opened from a fork the head
branch lives on the contributor's repo, and the workflow GITHUB_TOKEN
cannot push there, so commits ended up as a stray branch on this repo
and never landed on the PR.
Redirect origin's push URL to the PR head repository (the fork for fork
PRs, this repo otherwise) using a PAT secret (CLAUDE_BOT_PAT) that has
push access; fetches still come from origin. persist-credentials is
disabled so the PAT in the push URL is used instead of the GITHUB_TOKEN
auth header. Requires the fork PR to have "Allow edits by maintainers"
enabled.
Rename claude-issue-bot.yml to claude-bot.yml and broaden it beyond
issues:
- handle-pr: review pull requests on open (read diff, label, post one
grounded review comment); review-only, no code changes.
- mention: allow committing. Add Edit/Write and git tools, contents:
write, and instruct it to make the smallest correct change and commit
to the current branch only on an explicit code-change request. Kept
default user gating (no allowed_non_write_users) so only write-access
users can trigger commits.
- Refresh the repository map (add internal/eventbus and the
service/email subpackage) across all three prompts.
- Raise max-turns.
@