Add subprocess isolation setup and git credential helper (#1132)

- Add optional bubblewrap setup step for Linux subprocess isolation
  when allowed_non_write_users is configured
- Use git credential helper instead of embedding token in remote URL
- edit-issue-labels.sh: read issue number from workflow event payload
  instead of CLI arg
- Add CLAUDE_CODE_SCRIPT_CAPS env for per-script call limit config
- docs/security.md: note recommended github_token configuration

🏠 Remote-Dev: homespace
This commit is contained in:
Octavian Guzu 2026-03-31 12:36:51 +01:00 committed by GitHub
parent 7225f045c6
commit 32156b120b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 68 additions and 23 deletions

View File

@ -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. - 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: 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 post any comments explaining your decision
- DO NOT communicate directly with users - DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels - If no labels are clearly applicable, do not apply any labels

View File

@ -20,6 +20,8 @@ jobs:
- name: Run Claude Code for Issue Triage - name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-action@main
env:
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
with: with:
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}" prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

View File

@ -195,6 +195,26 @@ runs:
cd ${GITHUB_ACTION_PATH} cd ${GITHUB_ACTION_PATH}
bun install --production 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 - name: Run Claude Code Action
id: run id: run
shell: bash shell: bash
@ -214,6 +234,7 @@ runs:
ALLOWED_BOTS: ${{ inputs.allowed_bots }} ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }} 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_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 }} INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }} EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}

View File

@ -13,7 +13,9 @@
- Accepts either a comma-separated list of specific usernames or `*` to allow all users - 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 - **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 - 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 - **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 - **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 - **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions

View File

@ -421,7 +421,8 @@ jobs:
- `./scripts/gh.sh label list` to see available labels - `./scripts/gh.sh label list` to see available labels
Based on your analysis, add the appropriate labels using: 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. If it appears to be a duplicate, post a comment mentioning the original issue.

View File

@ -1,22 +1,26 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Edits labels on a GitHub issue. # 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 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=() ADD_LABELS=()
REMOVE_LABELS=() REMOVE_LABELS=()
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--issue)
ISSUE="$2"
shift 2
;;
--add-label) --add-label)
ADD_LABELS+=("$2") ADD_LABELS+=("$2")
shift 2 shift 2
@ -26,20 +30,12 @@ while [[ $# -gt 0 ]]; do
shift 2 shift 2
;; ;;
*) *)
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
exit 1 exit 1
;; ;;
esac esac
done 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 if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
exit 1 exit 1
fi fi

View File

@ -51,11 +51,34 @@ export async function configureGitAuth(
console.log("No existing authentication headers to remove"); console.log("No existing authentication headers to remove");
} }
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 // Update the remote URL to include the token for authentication
console.log("Updating remote URL with authentication..."); console.log("Updating remote URL with authentication...");
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`; const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
await $`git remote set-url origin ${remoteUrl}`; await $`git remote set-url origin ${remoteUrl}`;
console.log("✓ Updated remote URL with authentication token"); console.log("✓ Updated remote URL with authentication token");
}
console.log("Git authentication configured successfully"); console.log("Git authentication configured successfully");
} }