Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions .github/workflows/agent-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,58 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Install merge guard
run: |
mkdir -p /tmp/agent-task-guard
cat > /tmp/agent-task-guard/guard.py <<'PYTHON_GUARD'
#!/usr/bin/env python3
"""PreToolUse guard for accidental PR merges in agent-task jobs.

This is a command guardrail, not a security boundary against a hostile agent with
write access or credentials. Enforce hard restrictions with GitHub branch rules.
"""
import json
import re
import sys


def blocks(command):
# Inspect the whole shell input, including chained commands and substitutions.
# Overblocking commands that merely mention a merge is preferable to merging.
if re.search(r"\bgh\s+pr\s+merge\b", command, re.I):
return True
if not re.search(r"\bgh\s+api\b", command, re.I):
return False
if re.search(r"(?:/repos/|\brepos/)[^\s'\";]+/pulls/\d+/merge\b", command, re.I):
return True
if re.search(r"\b(?:mergePullRequest|enablePullRequestAutoMerge|enqueuePullRequest)\b", command, re.I):
return True
# A GraphQL query can also be loaded from a file, stdin, or shell expansion.
# Deny GraphQL writes rather than trying to interpret arbitrary shell input.
if re.search(r"\bgh\s+api\s+graphql\b", command, re.I):
return bool(re.search(r"\bmutation\b|--input\b|\$\(|<|@", command, re.I) or re.search(r"(?:^|\s)-F(?:\s|=)", command))
return False


def main():
try:
data = json.load(sys.stdin)
command = data["tool_input"]["command"]
if not isinstance(command, str):
raise ValueError("Missing Bash command")
except (ValueError, KeyError, TypeError):
print("Cannot inspect Bash command; blocked by merge guard", file=sys.stderr)
return 2
if data.get("tool_name") == "Bash" and blocks(command):
print("PR merges are disabled for agent-task jobs", file=sys.stderr)
return 2
return 0


if __name__ == "__main__":
sys.exit(main())
PYTHON_GUARD
chmod +x /tmp/agent-task-guard/guard.py
- name: Get plugin marketplace token
id: marketplace_auth
run: |
Expand All @@ -55,7 +107,7 @@ jobs:
echo "::add-mask::${MARKETPLACE_TOKEN}"
echo "token=${MARKETPLACE_TOKEN}" >> "$GITHUB_OUTPUT"

- uses: anthropics/claude-code-action@v1
- uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507
env:
GH_TOKEN: ${{ github.token }}
with:
Expand All @@ -67,8 +119,9 @@ jobs:
show_full_output: true
claude_args: |
--model claude-opus-5-5
--settings '{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"/tmp/agent-task-guard/guard.py","timeout":10}]}]}}'
--allowedTools "Read,Skill,Glob,Grep,LSP,WebFetch,WebSearch,Write,Edit,Bash(git:*),Bash(gh pr:*),Bash(gh api:*),Bash(jq:*)${{ inputs.extra_allowed_tools != '' && format(',{0}', inputs.extra_allowed_tools) || '' }}"
--disallowedTools "ScheduleWakeup"
--disallowedTools "ScheduleWakeup,Bash(gh pr merge:*)"
--append-system-prompt "You are running as a one-shot GitHub Actions job. There is no future turn, resumption, or wakeup — once you finish responding, this process ends permanently."
- name: Upload agent result
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
echo "::add-mask::${MARKETPLACE_TOKEN}"
echo "token=${MARKETPLACE_TOKEN}" >> "$GITHUB_OUTPUT"

- uses: anthropics/claude-code-action@v1
- uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507
with:
allowed_bots: "*"
anthropic_federation_rule_id: ${{ inputs.anthropic_federation_rule_id }}
Expand Down