Skip to content

feat(claude-lanes): add the intake-triage reusable lane - #659

Merged
kyle-sexton merged 4 commits into
mainfrom
feat/claude-intake-triage
Oct 3, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
feat/claude-intake-triage

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No related issue: first CI lane of the automation-lanes effort, planned in a local decision brief.

Hold: a human merges this. It adds a lane that holds a secret and issues: write, so it is a lane-power change; the babysit lane must not merge it.

Summary

Adds .github/workflows/claude-intake-triage.yml, a reusable workflow that triages a newly opened issue: a read-only Claude run proposes labels and a comment, and a step with no model validates and applies them. README gains its entry.

Fix

  • Claude runs with --permission-mode dontAsk, tools Read,Grep,Glob,Bash, Bash approved only for gh issue view/list, gh search issues, gh label list, and --json-schema output whose label enum is the caller's allowed-labels intersected with the repository's live labels.
  • allowed_non_write_users: "*" with the job GITHUB_TOKEN, so outside reporters are triaged (the action then scrubs subprocess secrets and keeps .git/config token-free).
  • The apply step keeps only allowlisted, existing labels (addLabels would otherwise create unknown ones), breaks @mentions, withholds a comment matching a credential pattern, and applies escalation-label (default needs-human) on any permission denial, a max-turns stop, or needs_human: true.
  • The CLI is installed at run time and passed through path_to_claude_code_executable (cli-version input, else the CLAUDE_LANE_CLI_VERSION variable, else latest; bundled keeps the action's own). The step summary records the installed version and the npm-published latest.
  • Job claude-intake-triage-status goes red on a failed run; it is advisory and must never be required.

Verification

  • actionlint and zizmor --offline: no findings.
  • node --test .github/scripts/*.test.cjs: 183 pass, 2 fail; both failures (ci-fanout-consolidation, V2 architecture doc exists) read files under docs/topics/, which chore: remove docs/topics #650 removed, and fail on main too. The outcome-wiring test now covers this lane.
  • Local probes on Claude Code CLI 2.1.288: dontAsk denied Read of /etc/hostname and /proc/self/status and recorded both in permission_denials while reading a file in the working directory; --tools restricted to Read,Grep,Glob,Bash plus --json-schema with a label enum returned valid structured_output.
  • Not yet run end to end: that needs the standards runner-policy contract for the merged SHA and a synced caller; the pilot on claude-code-plugins includes a prompt-injection canary issue.

Related

  • Pattern mirrored: claude-review.yml (outcome composite, status job).
  • Coexists with issue-triage-label.yml (deterministic needs-triage floor).
  • Next: standards runner-policy contract and caller component, then the claude-code-plugins pilot.

🤖 Generated with Claude Code

A read-only Claude run (dontAsk, read-only gh queries, JSON schema output
whose label enum is the caller allowlist intersected with live repository
labels) proposes labels and a comment for a newly opened issue. A step with
no model applies only allowlisted, existing labels, breaks @mentions,
withholds a comment that looks like a credential, and applies the
escalation label on a permission denial, a max-turns stop or a request for
a person. The lane installs the newest Claude Code CLI at run time through
path_to_claude_code_executable, with a cli-version input and a
CLAUDE_LANE_CLI_VERSION variable to fall back to stable or bundled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 3, 2026 19:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T19:16:23.572736Z e222faf Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude code review in progress

  • Gather context (diff, CLAUDE.md/REVIEW.md, AGENTS.md)
  • Review .github/workflows/claude-intake-triage.yml
  • Review README.md changes
  • Check alignment with PR body contract / REVIEW.md criteria
  • Post findings

View job run

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 43s —— View job


Security review complete

  • Gather context (PR diff, workflow file, custom instructions)
  • Review .github/workflows/claude-intake-triage.yml for logic/trust-boundary issues
  • Review README.md changes (documentation only, no finding)
  • Check for instruction-surface deletions (none — this PR only adds; no CLAUDE.md/AGENTS.md/rules file content was removed or softened)
  • Adversarially verify candidate finding with an independent subagent
  • Post findings

Reviewed only the two changed files (.github/workflows/claude-intake-triage.yml, README.md), scoped to logic, trust-boundary, and data-flow issues — not GitHub Actions hardening (dangerous triggers, permission widening, unpinned actions), which is zizmor's advisory lane and out of scope here.

1 IMPORTANT finding, posted inline at
L329-341:

The "Apply the triage" step's createComment call is not gated by the escalation state. labels is built from the model's output before the escalation check runs, and the escalation label is only ever added on top — it never suppresses anything. So a run that hit a permission denial, stopped at max turns, or itself set needs_human: true still gets its proposed labels applied and its free-text comment posted live to the public issue, with no gate beyond a credential-pattern check and @mention-breaking (no URL/link filtering). This contradicts the file's own header claim that escalation applies "instead of trusting the run" (L39-40), and the posted comment's footer ("A maintainer reviews it.", L339) is factually inaccurate — nothing reviews it before it goes out. Since allowed_non_write_users: "*" lets any unauthenticated reporter open the triggering issue, a partially-successful prompt injection (one that gets the model to echo attacker-chosen text and flags needs_human) still results in that attacker-shaped text posting under an apparently-trusted bot marker. Suggested fix: if (comment && escalate.length === 0), and reword the footer.

Everything else held up under review: the label allowlist is double-enforced (schema enum + re-check against live repo labels), the Bash tool is restricted to read-only gh queries with --permission-mode dontAsk, the CLI-version input is regex-validated before being passed to the install script, and the credential/@mention scrubbing on the comment itself is sound.
· Branch

Comment thread .github/workflows/claude-intake-triage.yml Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e222faf5b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/claude-intake-triage.yml Outdated
Comment thread .github/workflows/claude-intake-triage.yml Outdated
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

claude-security-review has reviewed this pull request through 35c4eb7; a later push is reviewed from there.

kyle-sexton added a commit that referenced this pull request Oct 3, 2026
No related issue: main has been red since c11c7ec (#650); this restores
it.

## Summary

#650 deleted `docs/topics/` while #656 added two AGENTS.md links into
it, and two tests still read files there. `lychee-offline` and
`selector-contract` fail on `main` and on every open PR.

## Fix

- AGENTS.md: the two Code Review Rules lines that linked into
`docs/topics/` now state their rule inline.
- `.github/scripts/ci-fanout-consolidation.test.cjs`: drop the ADR read
and the test that asserted the ADR's text.
- `.github/scripts/claude-review-plugin-path.test.cjs`: drop the test
that asserted the V2 architecture doc's text.

## Verification

- `node --test .github/scripts/*.test.cjs`: 192 pass, 0 fail.
- `git grep docs/topics` finds only the standards-managed
`.github/actionlint.yaml` comment that #650 already noted.

## Related

- Unblocks #659.
- Cause: #650 and #656 merged in parallel.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 54s —— View job


Code review complete

  • Gather context (diff, REVIEW.md, AGENTS.md)
  • Review .github/workflows/claude-intake-triage.yml (correctness/design/conventions — security is out of scope here, owned by the separate claude-security-review lane per REVIEW.md)
  • Review README.md changes
  • Adversarially verify candidate findings with an independent subagent
  • Post findings

This is the code-review lane: scoped to correctness, design, error handling, and maintainability. Security findings (the escalation/comment-gating issue already flagged inline by the security review and Codex's empty-enum catch) belong to the security lane and Codex respectively, and are intentionally omitted here to avoid duplicate reporting across lanes.

2 findings posted inline, both independently verified:

  1. Important — L305-330: a nominally-"successful" run (subtype: "success", no permission denials, no max-turns stop) whose structured output is empty, unparseable, or shape-mismatched gets silently dropped: output becomes null, so neither the labels loop nor the needs_human check contributes anything to escalate, and the escalation label is never applied. The issue is left completely untouched — no labels, no comment, no escalation marker — even though the job reports green. This escapes the file's own stated model ("...applies the escalation label instead of trusting the run"), which only accounts for permission denials, max-turns, and needs_human: true, not "success" with unusable output. claude-lane-outcome/classify.cjs already documents that subtype: "success" can coexist with is_error: true, confirming "success" isn't a trustworthiness guarantee.

  2. Suggestion — L332-341: addLabels and createComment are two unguarded, independent writes with no atomicity between them (per REVIEW.md's atomicity criterion). If the comment call throws after labels were already applied, the step fails before the job-summary write ever runs, leaving an issue labeled (possibly with the escalation label) with no explanation and no summary record of what happened. Lower severity since each call is idempotent on rerun, but worth a try/catch or reordering (comment before labels) so a partial failure doesn't leave the less-safe half standing.

Everything else held up: the label resolution, schema composition, CLI version handling, and job-status wiring are internally consistent, and the README addition accurately describes the current behavior.
· Branch

Comment thread .github/workflows/claude-intake-triage.yml Outdated
Comment thread .github/workflows/claude-intake-triage.yml Outdated
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

claude-review has reviewed this pull request through 35c4eb7; a later push is reviewed from there.

…tes retry-safe

An escalated run (permission denial, max turns, unusable output, a
credential-like comment, or needs_human) now applies only the escalation
label and a fixed note instead of the model's labels and text, and a
finished run with unusable output escalates instead of leaving the issue
untouched. An empty allowlist yields labels maxItems 0 rather than an
invalid empty enum. The comment is written before the labels, a rerun edits
the lane's marker comment, and a write failure still records the summary.

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

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 36s —— View job


Code review complete (incremental: 007a944…3041dd3)

  • Read .claude-lane/incremental.diff (the only diff in scope) and gh pr diff for context
  • Review the changes to .github/workflows/claude-intake-triage.yml
  • Review the changes to README.md
  • Check the new logic against the four prior review findings it's meant to fix
  • Post findings

This push is a fix-up: it addresses all four previously-flagged findings on this PR.

  • Escalation now gates labels and the comment (security review's Important finding): an escalated run (escalated = ran && escalate.length > 0, L339) clears the model's labels/comment and substitutes only the escalation label plus a fixed, non-attacker-influenced string (L340-344). The prompt-injection-to-public-comment path from the prior review is closed.
  • Unusable structured output now escalates instead of going silent (code-review's Important finding): if (ran && !output) escalate.push(...) (L322) feeds the same escalation branch, so a nominally-"success" run with empty/malformed output now gets the escalation label instead of leaving the issue untouched.
  • Atomicity (code-review's Suggestion): the comment write now happens before addLabels, both inside one try/catch that records failure into the step summary and calls core.setFailed (L353-369, L382, L385), so a partial failure surfaces instead of disappearing.
  • Empty label enum (Codex's P2): the schema now emits { type: "array", maxItems: 0 } for labels when nothing is allowed, instead of an invalid enum: [] (L203-207).
  • Also picked up Codex's rerun-dedup suggestion unprompted: the apply step now looks up the lane's earlier marker comment and updates it instead of always creating a new one (L356-363).

1 finding posted inline (Suggestion, README.md:815): the rewritten claude-intake-triage.yml doc entry lists four escalation triggers (permission denial, max-turns stop, unusable output, request for a person) but omits the fifth one the code and this same diff's own workflow-header rewrite both include — a credential-like comment. Low severity (doc completeness only), but worth closing since this paragraph was specifically rewritten in this diff to enumerate the triggers.

Everything else in the diff held up: the ran/escalated/!ran control flow has no reachable path that both posts the model's own text and escalates, the comment-update marker lookup correctly scopes to github-actions[bot]-authored comments carrying the marker, and the new summary line (Write error) correctly reflects the try/catch outcome.
· Branch

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 2s —— View job


Security review complete

  • Gather context (incremental diff since 007a944, prior review findings)
  • Review the incremental diff (claude-intake-triage.yml, README.md)
  • Check whether prior findings were addressed
  • Apply instruction-surface deletion lens (no rules/CLAUDE.md/AGENTS.md touched — n/a)
  • Post findings

No security issues found in this increment.

Scope: only what changed since 007a944 in .github/workflows/claude-intake-triage.yml and README.md (per .claude-lane/incremental.diff), in the "Apply the triage" step (around L282-376) and the schema-composition step's enum fallback (around L198-205).

This commit closes the trust-boundary gap flagged in the prior round:

  • Comment/label gating now matches the escalation claim. escalated = ran && escalate.length > 0 (L337) now clears the model's proposed labels and comment and substitutes a fixed canned message whenever any escalation reason fires — permission denial, max-turns, needs_human, a credential-shaped comment, or unusable structured output. The credential branch in particular no longer just blanks comment and moves on; it now routes through the same escalated path, so a run that trips the credential regex can no longer have its labels silently applied. This removes the exploit path from the earlier review: a partially-successful prompt injection that gets attacker-shaped text into comment while also setting needs_human: true (or hitting a denial) can no longer get that text posted — the fixed text replaces it.
  • The silent-success gap is closed. if (ran && !output) escalate.push(...) (L321) means a nominally-successful run with empty/unparseable/shape-mismatched output now escalates instead of leaving the issue untouched with a green job.
  • Rerun dedup is safe. The lookup that finds an "earlier" comment to update instead of duplicating is gated on c.user?.login === "github-actions[bot]" and the literal MARKER prefix (L350-356) — that login belongs to the job's own GITHUB_TOKEN identity and can't be spoofed by an issue reporter, so this can't be tricked into editing an unrelated or attacker-authored comment.
  • Write ordering + error handling. Comment before labels, wrapped in try/catch with the error surfaced via core.setFailed and the job summary (L360-376) — a partial failure now fails the job loudly rather than leaving a silent inconsistent state. (Atomicity/ordering is a code-review-lane concern per scope; noted here only because it was part of the prior finding set this commit closes.)
  • Codex's empty-enum P2 is also fixed: allowed.length > 0 ? {...enum...} : { type: "array", maxItems: 0 } (L200-203) avoids the invalid empty enum: [] case.

No new trust-boundary, injection, or credential-handling issues were introduced by this diff. The canned escalation message is static text with no model- or issue-controlled interpolation, so it carries no injection surface. Nothing else in scope changed.

Instruction-surface lens: this diff touches only the workflow file and README — no CLAUDE.md/AGENTS.md/rules file content was deleted, narrowed, or softened, so that lens doesn't apply here.
· Branch

Comment thread README.md Outdated
…calation

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

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 26s —— View job


Code review complete (incremental: 3041dd3…35c4eb7)

  • Gather context (incremental diff since 3041dd3, custom instructions)
  • Invoke /review:code-review for this PR
  • Review README.md changes (the only file in scope this round)
  • Post findings

This increment is a one-line doc fix addressing the prior code-review finding exactly: the claude-intake-triage.yml entry's escalation-trigger list now reads "a permission denial, a max-turns stop, unusable output, a credential-like comment or a request for a person" (README.md:816), matching both the workflow header's own enumeration and the escalate.push(...) branches in the apply step (unchanged since 3041dd3, already reviewed).

No findings. The added clause is accurate, reads correctly with the existing line wrap, and closes the gap the previous round flagged. Nothing else in this file changed.
· Branch

@kyle-sexton
kyle-sexton merged commit 632130a into main Oct 3, 2026
27 checks passed
@kyle-sexton
kyle-sexton deleted the feat/claude-intake-triage branch October 3, 2026 19:39
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