diff --git a/.claude/commands/label-issue.md b/.claude/commands/label-issue.md index 62497ef..a151b03 100644 --- a/.claude/commands/label-issue.md +++ b/.claude/commands/label-issue.md @@ -45,7 +45,7 @@ TASK OVERVIEW: - If you find similar issues using ./scripts/gh.sh search, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. 5. Apply the selected labels: - - Use `./scripts/edit-issue-labels.sh --issue NUMBER --add-label LABEL1 --add-label LABEL2` to apply your selected labels + - Use `./scripts/edit-issue-labels.sh --add-label LABEL1 --add-label LABEL2` to apply your selected labels (issue number is read from the workflow event) - DO NOT post any comments explaining your decision - DO NOT communicate directly with users - If no labels are clearly applicable, do not apply any labels diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index f889591..b713a39 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -20,6 +20,8 @@ jobs: - name: Run Claude Code for Issue Triage uses: anthropics/claude-code-action@main + env: + CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}' with: prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/action.yml b/action.yml index 9e830cb..41db76a 100644 --- a/action.yml +++ b/action.yml @@ -195,6 +195,26 @@ runs: cd ${GITHUB_ACTION_PATH} bun install --production + - name: Install subprocess isolation dependencies + # Install subprocess isolation dependencies when processing content from non-write users. + # Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners). + if: ${{ inputs.allowed_non_write_users != '' && env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB != '0' && runner.os == 'Linux' }} + continue-on-error: true + shell: bash + run: | + if command -v apt-get >/dev/null && command -v sudo >/dev/null; then + for i in 1 2 3; do + sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break + echo "apt-get attempt $i failed, retrying..." + sleep 5 + done + fi + # Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor. + # The sysctl doesn't exist on older kernels — that's fine. + if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + fi + - name: Run Claude Code Action id: run shell: bash @@ -214,6 +234,7 @@ runs: ALLOWED_BOTS: ${{ inputs.allowed_bots }} ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }} CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: ${{ env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB || (inputs.allowed_non_write_users != '' && '1') || '' }} + CLAUDE_CODE_SCRIPT_CAPS: ${{ env.CLAUDE_CODE_SCRIPT_CAPS || '' }} INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }} EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }} GITHUB_RUN_ID: ${{ github.run_id }} diff --git a/docs/security.md b/docs/security.md index 273a673..f4fb736 100644 --- a/docs/security.md +++ b/docs/security.md @@ -13,7 +13,9 @@ - Accepts either a comma-separated list of specific usernames or `*` to allow all users - **Should be used with extreme caution** as it bypasses the primary security mechanism of this action - Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope - - When set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets from subprocess environments. This reduces but does not eliminate prompt injection risk — keep workflow permissions minimal and validate all outputs. Set `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: 0` in your workflow or job `env:` block to opt out. + - When set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets from subprocess environments. On Linux runners with bubblewrap available, subprocesses additionally run with PID-namespace isolation. This reduces but does not eliminate prompt injection risk — keep workflow permissions minimal and validate all outputs. Set `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: 0` in your workflow or job `env:` block to opt out. + - Optionally set `CLAUDE_CODE_SCRIPT_CAPS` in your workflow `env:` block to limit how many times Claude can call specific scripts per run. Value is JSON: `{"script-name.sh": maxCalls}`. Example: `CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'` allows at most 2 calls to `edit-issue-labels.sh`. Useful for write-capable helper scripts. + - When using `allowed_non_write_users`, always pass `github_token: ${{ secrets.GITHUB_TOKEN }}`. The auto-generated workflow token is scoped to the job's declared permissions and expires automatically, which limits blast radius. Personal access tokens are not recommended for untrusted-input workflows. - **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in - **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered - **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions diff --git a/docs/solutions.md b/docs/solutions.md index dd7fcb5..088d6cf 100644 --- a/docs/solutions.md +++ b/docs/solutions.md @@ -421,7 +421,8 @@ jobs: - `./scripts/gh.sh label list` to see available labels Based on your analysis, add the appropriate labels using: - `./scripts/edit-issue-labels.sh --issue [number] --add-label "label1" --add-label "label2"` + `./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"` + (the issue number is read automatically from the workflow event) If it appears to be a duplicate, post a comment mentioning the original issue. diff --git a/scripts/edit-issue-labels.sh b/scripts/edit-issue-labels.sh index d160a55..a670c4b 100755 --- a/scripts/edit-issue-labels.sh +++ b/scripts/edit-issue-labels.sh @@ -1,22 +1,26 @@ #!/usr/bin/env bash # # Edits labels on a GitHub issue. -# Usage: ./scripts/edit-issue-labels.sh --issue 123 --add-label bug --add-label needs-triage --remove-label untriaged +# Usage: ./scripts/edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged +# +# The issue number is read from the workflow event payload. # set -euo pipefail -ISSUE="" +# Read from event payload so the issue number is bound to the triggering event +ISSUE=$(jq -r '.issue.number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}") +if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then + echo "Error: no issue number in event payload" >&2 + exit 1 +fi + ADD_LABELS=() REMOVE_LABELS=() # Parse arguments while [[ $# -gt 0 ]]; do case $1 in - --issue) - ISSUE="$2" - shift 2 - ;; --add-label) ADD_LABELS+=("$2") shift 2 @@ -26,20 +30,12 @@ while [[ $# -gt 0 ]]; do shift 2 ;; *) + echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2 exit 1 ;; esac done -# Validate issue number -if [[ -z "$ISSUE" ]]; then - exit 1 -fi - -if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then - exit 1 -fi - if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then exit 1 fi diff --git a/src/github/operations/git-config.ts b/src/github/operations/git-config.ts index 97e02be..3df584b 100644 --- a/src/github/operations/git-config.ts +++ b/src/github/operations/git-config.ts @@ -51,11 +51,34 @@ export async function configureGitAuth( console.log("No existing authentication headers to remove"); } - // Update the remote URL to include the token for authentication - console.log("Updating remote URL with authentication..."); - const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`; - await $`git remote set-url origin ${remoteUrl}`; - console.log("✓ Updated remote URL with authentication token"); + if (process.env.ALLOWED_NON_WRITE_USERS) { + // When processing content from non-write users, use a credential helper + // instead of embedding the token in the remote URL. The helper script reads + // from GH_TOKEN at auth time, so .git/config stays token-free. Written as a + // file to avoid shell-escaping the helper body; placed under + // GITHUB_ACTION_PATH so it sits alongside the action source. + console.log("Configuring git credential helper..."); + process.env.GH_TOKEN = githubToken; + const helperPath = join( + process.env.GITHUB_ACTION_PATH || homedir(), + ".git-credential-gh-token", + ); + await writeFile( + helperPath, + '#!/bin/sh\necho username=x-access-token\necho password="$GH_TOKEN"\n', + { mode: 0o700 }, + ); + const cleanUrl = `https://${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`; + await $`git remote set-url origin ${cleanUrl}`; + await $`git config credential.helper ${helperPath}`; + console.log("✓ Configured credential helper"); + } else { + // Update the remote URL to include the token for authentication + console.log("Updating remote URL with authentication..."); + const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`; + await $`git remote set-url origin ${remoteUrl}`; + console.log("✓ Updated remote URL with authentication token"); + } console.log("Git authentication configured successfully"); }