Skip to content

pr-shepherd: dry_run is enforced by the prompt, not by the tool allowlist #204

Description

@realmarcin

From the review of #194.

pr-shepherd.yml grants Bash(gh pr comment:*) unconditionally:

--allowedTools "Bash(gh pr list:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checks:*),Bash(git log:*),Bash(git diff:*),Bash(gh pr comment:*),Read,Grep,Glob"

dry_run defaults to "true", and the only thing stopping a comment in that mode is a prompt instruction:

If DRY_RUN is "true", post NOTHING. Report your assessment as your final message only.

So the default, safest mode is guaranteed by persuasion rather than by capability. A model that misreads the input, or a prompt-injection payload in a PR diff it is reading, could post anyway. The workflow reads untrusted PR content by design, which is exactly the setting where instruction-level limits are weakest.

Why this is worth fixing rather than shrugging at

The PR argues the point itself, and applies it correctly elsewhere. gh api is deliberately excluded from the allowlist with this reasoning:

It is a general HTTP client for the API, so allowlisting it would permit anything the token can do — including submitting an approving review.

That is capability-based reasoning: make the bad action impossible, not merely forbidden. The prompt calls its limits "hard limits, not preferences". dry_run is the one place where that principle is stated but not implemented — and it is the default mode.

Suggested fix

Make the grant conditional, so in dry-run the tool is simply absent:

--allowedTools "Bash(gh pr list:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checks:*),Bash(git log:*),Bash(git diff:*),Read,Grep,Glob${{ inputs.dry_run == 'false' && ',Bash(gh pr comment:*)' || '' }}"

Keep the prompt instruction as well — belt and braces — but the allowlist becomes the thing that actually holds.

Worth confirming the rendered string is well-formed for both input values before merging, since claude_args is a literal block scalar passed straight through as CLI arguments.

Note

The same reasoning applies to any future capability added to this agent: if the prompt says it must not do X, prefer not granting X over asking it not to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions