Skip to content

fix(ci): allow jq in issue-labeler assign-label step - #1367

Merged
jth-nw merged 4 commits into
devfrom
fix/issue-labeler-jq-permission
Aug 12, 2026
Merged

fix(ci): allow jq in issue-labeler assign-label step#1367
jth-nw merged 4 commits into
devfrom
fix/issue-labeler-jq-permission

Conversation

@jth-nw

Copy link
Copy Markdown
Collaborator

Summary

  • The Step 3 — Assign label job in claude-issue-labeler.yml restricts tool use to Bash(gh:*), so any gh ... | jq ... pipe the agent tries gets denied.
  • Traced this via issue Feedback: https://docs.netwrix.com/docs/identitymanager/current/integration-guide/api/server/ #1361: that run hit 4 permission denials and 19 turns (vs. a normal ~7 turns / 0 denials) and never actually applied a label to the issue, despite the step reporting success.
  • Widens the allowlist to Bash(gh:*),Bash(jq:*),Skill(assign-label) so filtering gh JSON output through jq no longer gets blocked.

Test plan

  • Open/edit a test issue and confirm Step 3 — Assign label completes with permission_denials_count: 0 and the issue receives the expected labels.

The assign-label step's tool allowlist only permitted `gh`, so any
`gh ... | jq ...` pipe the agent tried got denied — burning turns on
retries and sometimes leaving the issue with no label at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ailures
Address code review feedback on the jq allowlist fix:
- Extend Bash(jq:*) to Steps 1 and 2 as well, so all three
claude-code-action steps have consistent tool access instead of
leaving two of three still degraded.
- Add Read,Grep,Glob to Step 3 to match Steps 1/2, since the sparse
checkout includes .github/.claude and the agent may reasonably try
to inspect local files.
- Turn Step 4's silent "no labels found" log line into a `::warning::`
annotation so a repeat of #1361 (assign-label reporting success but
applying nothing) surfaces in the run summary instead of disappearing.
Note: whether jq alone fully resolves the original denials is still
unverified — Claude Code's Bash permission engine denies command
substitution (`$(...)`) outright regardless of allowlist, which is a
plausible alternate cause. Confirming that requires a live run with
show_full_output: true, which wasn't done here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n opened issues
Address second round of review feedback:
- Bash(jq:*) was a mistake: standalone jq can dump the whole process
environment (`jq -n env`) and read arbitrary local files
(`--rawfile`, `-R`), and Steps 1-3 process fully attacker-controlled
issue/comment content with allowed_non_write_users: '*'. Combined
with Bash(gh:*) (which can post comments), that's a prompt-injection
path to exfiltrate ANTHROPIC_API_KEY out of the process environment.
None of the three skills' documented flows use standalone jq anyway
(identify-security-vuln-discussion and code-of-conduct-check use no
jq at all; assign-label only uses gh's own --jq flag) — this was
guesswork addressing an unverified failure, not a real requirement.
- Removed Bash(jq:*) from all three steps and added an explicit prompt
line telling the agent to use gh's built-in --jq instead of piping
to a separate jq process. This also sidesteps the original
permission-denial theory by construction: a single `gh ... --jq`
call never crosses a pipe boundary, so it can't hit the
Bash-permission-checks-each-pipeline-segment issue in the first
place, regardless of whether that was the real cause of #1361's
denials.
- The zero-labels warning only fires when an issue ends with no
labels at all, which misses #1361-style cases where template labels
are present but Step 3 added nothing on top. Added a same-set
comparison (pre vs. post Step 3) scoped to newly-opened issues,
which is the case where "no change" is most likely a silent
failure rather than a legitimate conservative decision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Code Review

Reviewed the net diff against dev (.github/workflows/claude-issue-labeler.yml, +18/−2). No blocking correctness or security bugs. Two things worth addressing before merge, both non-blocking.

1. PR title/description no longer match the diff (medium)

The title says "allow jq in issue-labeler assign-label step" and the body says the change "widens the allowlist to Bash(gh:*),Bash(jq:*),Skill(assign-label)". The final diff does not add Bash(jq:*) anywhere. Branch history shows why:

  • 4e8bbc23 added Bash(jq:*) to Step 3
  • e445ea5e added Bash(jq:*) + Read,Grep,Glob to all three steps
  • f0af124d reverted all Bash(jq:*) additions in favor of the gh --jq prompt guidance

So the net change is: prompt guidance to prefer gh --jq (all 3 steps), Read,Grep,Glob added to Step 3 for parity with Steps 1–2, plus the new label-drift warning.

The functional consequence: the stated root cause (gh ... | jq ... pipes hitting Bash(gh:*) and getting denied) is now mitigated advisorily rather than structurally. assign-label/SKILL.md:27 already uses --jq, so the documented path is clean — but nothing prevents the model from reaching for a standalone jq pipe again, and the test plan's success criterion is permission_denials_count: 0. Either add Bash(jq:*) if you want the hard guarantee, or update the title/body so the commit history doesn't claim a change that isn't there.

2. Label-drift warning will false-positive on legitimate no-op runs (low)

claude-issue-labeler.yml:126-132 warns "It may have failed silently" whenever the pre/post label sets are identical on an opened event. But assign-label/SKILL.md:50 explicitly instructs the skill to be conservative — "If ambiguous, leave it off — better to under-label than to mislabel." An issue opened from a template that already carries the right labels is a correct no-op, and it will now emit a misleading failure annotation. Consider softening the wording (e.g. "added no labels on top of template labels — verify this was intentional") since the check can't distinguish a correct no-op from a silent failure.

Related: both new checks emit ::warning:: only, so a genuine silent failure still doesn't fail the run. Fine if the intent is observability while you gather data on frequency — just noting the check won't stop a recurrence of #1361, only annotate it.

Verified as correct

  • Drift comparison logicjoin(labels.*.name, ',')tr ',' '\n'sed '/^$/d'sort -u round-trips safely. Confirmed no label in this repo contains a comma (gh label list, 0 matches), and label names with spaces (good first issue) survive the split correctly. Both sides use the same sort, so collation is consistent.
  • PRE_LABELS baseline is valid — Steps 1 and 2 (identify-security-vuln-discussion, code-of-conduct-check) don't mutate labels, so the event-payload snapshot is still accurate by the time Step 4 reads post-Step-3 labels. No stale-baseline race.
  • Empty-set handling — no labels at all falls through to the existing zero-label branch and exit 0 before the drift check; empty PRE_LABELS vs. non-empty post correctly reports "changed."
  • Quoting/set -e — nested command substitution in the ::warning:: echo is correctly quoted; untrusted values (PRE_LABELS, ISSUE_TITLE, ISSUE_ACTION) are passed via env: rather than inlined into the script body. No shell injection.
  • YAML validityPRE_LABELS: ${{ join(...) }} is a valid plain scalar (starts with $, not {), matching the already-in-production pattern at line 100. Prompt additions sit correctly inside the | block scalars.
  • Permissions/secrets — unchanged. No new secrets referenced; job scopes stay contents: read, issues: write, id-token: write.

Note on the added Read,Grep,Glob (informational)

Step 3 now matches Steps 1–2. Since this job runs with allowed_non_write_users: '*' and interpolates untrusted issue.title directly into the prompt, adding filesystem read tools nominally widens the prompt-injection surface — but the sparse checkout limits reads to .github, .claude, and CODE_OF_CONDUCT.md (all public), and Bash(gh:*) already permitted API reads and comment writes. No new secret exposure; flagging only for awareness.

No issues found in products.js, docusaurus.config.js, sidebars, or the KB copy pipeline — this PR touches none of them.

@jth-nw
jth-nw merged commit dc87064 into devAug 12, 2026
8 checks passed
Sign up for freeto 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

@jth-nw