Skip to content

Commit 32156b1

Browse files
authored
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 :house: Remote-Dev: homespace
1 parent 7225f04 commit 32156b1

File tree

7 files changed

+68
-23
lines changed

7 files changed

+68
-23
lines changed

.claude/commands/label-issue.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ TASK OVERVIEW:
4545
- 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.
4646

4747
5. Apply the selected labels:
48-
- Use `./scripts/edit-issue-labels.sh --issue NUMBER --add-label LABEL1 --add-label LABEL2` to apply your selected labels
48+
- 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)
4949
- DO NOT post any comments explaining your decision
5050
- DO NOT communicate directly with users
5151
- If no labels are clearly applicable, do not apply any labels

.github/workflows/issue-triage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020

2121
- name: Run Claude Code for Issue Triage
2222
uses: anthropics/claude-code-action@main
23+
env:
24+
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
2325
with:
2426
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}"
2527
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

action.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,26 @@ runs:
195195
cd ${GITHUB_ACTION_PATH}
196196
bun install --production
197197
198+
- name: Install subprocess isolation dependencies
199+
# Install subprocess isolation dependencies when processing content from non-write users.
200+
# Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners).
201+
if: ${{ inputs.allowed_non_write_users != '' && env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB != '0' && runner.os == 'Linux' }}
202+
continue-on-error: true
203+
shell: bash
204+
run: |
205+
if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
206+
for i in 1 2 3; do
207+
sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
208+
echo "apt-get attempt $i failed, retrying..."
209+
sleep 5
210+
done
211+
fi
212+
# Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
213+
# The sysctl doesn't exist on older kernels — that's fine.
214+
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then
215+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
216+
fi
217+
198218
- name: Run Claude Code Action
199219
id: run
200220
shell: bash
@@ -214,6 +234,7 @@ runs:
214234
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
215235
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
216236
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: ${{ env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB || (inputs.allowed_non_write_users != '' && '1') || '' }}
237+
CLAUDE_CODE_SCRIPT_CAPS: ${{ env.CLAUDE_CODE_SCRIPT_CAPS || '' }}
217238
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
218239
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
219240
GITHUB_RUN_ID: ${{ github.run_id }}

docs/security.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
- Accepts either a comma-separated list of specific usernames or `*` to allow all users
1414
- **Should be used with extreme caution** as it bypasses the primary security mechanism of this action
1515
- Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope
16-
- 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.
16+
- 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.
17+
- 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.
18+
- 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.
1719
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
1820
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
1921
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions

docs/solutions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ jobs:
421421
- `./scripts/gh.sh label list` to see available labels
422422

423423
Based on your analysis, add the appropriate labels using:
424-
`./scripts/edit-issue-labels.sh --issue [number] --add-label "label1" --add-label "label2"`
424+
`./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"`
425+
(the issue number is read automatically from the workflow event)
425426

426427
If it appears to be a duplicate, post a comment mentioning the original issue.
427428

scripts/edit-issue-labels.sh

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
#!/usr/bin/env bash
22
#
33
# Edits labels on a GitHub issue.
4-
# Usage: ./scripts/edit-issue-labels.sh --issue 123 --add-label bug --add-label needs-triage --remove-label untriaged
4+
# Usage: ./scripts/edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
5+
#
6+
# The issue number is read from the workflow event payload.
57
#
68

79
set -euo pipefail
810

9-
ISSUE=""
11+
# Read from event payload so the issue number is bound to the triggering event
12+
ISSUE=$(jq -r '.issue.number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
13+
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
14+
echo "Error: no issue number in event payload" >&2
15+
exit 1
16+
fi
17+
1018
ADD_LABELS=()
1119
REMOVE_LABELS=()
1220

1321
# Parse arguments
1422
while [[ $# -gt 0 ]]; do
1523
case $1 in
16-
--issue)
17-
ISSUE="$2"
18-
shift 2
19-
;;
2024
--add-label)
2125
ADD_LABELS+=("$2")
2226
shift 2
@@ -26,20 +30,12 @@ while [[ $# -gt 0 ]]; do
2630
shift 2
2731
;;
2832
*)
33+
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
2934
exit 1
3035
;;
3136
esac
3237
done
3338

34-
# Validate issue number
35-
if [[ -z "$ISSUE" ]]; then
36-
exit 1
37-
fi
38-
39-
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
40-
exit 1
41-
fi
42-
4339
if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
4440
exit 1
4541
fi

src/github/operations/git-config.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,34 @@ export async function configureGitAuth(
5151
console.log("No existing authentication headers to remove");
5252
}
5353

54-
// Update the remote URL to include the token for authentication
55-
console.log("Updating remote URL with authentication...");
56-
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
57-
await $`git remote set-url origin ${remoteUrl}`;
58-
console.log("✓ Updated remote URL with authentication token");
54+
if (process.env.ALLOWED_NON_WRITE_USERS) {
55+
// When processing content from non-write users, use a credential helper
56+
// instead of embedding the token in the remote URL. The helper script reads
57+
// from GH_TOKEN at auth time, so .git/config stays token-free. Written as a
58+
// file to avoid shell-escaping the helper body; placed under
59+
// GITHUB_ACTION_PATH so it sits alongside the action source.
60+
console.log("Configuring git credential helper...");
61+
process.env.GH_TOKEN = githubToken;
62+
const helperPath = join(
63+
process.env.GITHUB_ACTION_PATH || homedir(),
64+
".git-credential-gh-token",
65+
);
66+
await writeFile(
67+
helperPath,
68+
'#!/bin/sh\necho username=x-access-token\necho password="$GH_TOKEN"\n',
69+
{ mode: 0o700 },
70+
);
71+
const cleanUrl = `https://${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
72+
await $`git remote set-url origin ${cleanUrl}`;
73+
await $`git config credential.helper ${helperPath}`;
74+
console.log("✓ Configured credential helper");
75+
} else {
76+
// Update the remote URL to include the token for authentication
77+
console.log("Updating remote URL with authentication...");
78+
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
79+
await $`git remote set-url origin ${remoteUrl}`;
80+
console.log("✓ Updated remote URL with authentication token");
81+
}
5982

6083
console.log("Git authentication configured successfully");
6184
}

0 commit comments

Comments
 (0)