Skip to content

gh pr create / gh issue create body via $(cat <<EOF) defeats Claude Code permission matching #82

Description

@sebastientaggart

Summary

Several CodeCannon skills pass multi-line --body content to gh via
--body "$(cat <<'EOF' ... EOF)". In Claude Code, this pattern breaks static
permission matching: the harness sees an embedded command substitution and
refuses to extend any Bash(gh:*) allow rule over it, so it falls back to a
Yes/No permission prompt on every invocation — with no "Allow always" option,
because there is no stable pattern to remember. Users who run these skills
frequently get interrupted once per PR creation and once per follow-up issue,
even with broad gh:* allow rules in place.

The simple gh calls in the same skills (gh pr view --json …, gh issue list,
gh pr merge, gh label create) are fine — they match Bash(gh:*) cleanly
and never prompt. Only the body-via-substitution calls are affected.

Affected locations

  • skills/submit-for-review.md lines ~127-135 — gh pr create in Step 6
  • skills/submit-for-review.md lines ~263-277 — gh issue create in the
    follow-up issues step
  • skills/deploy.md — the gh pr create and gh release create blocks (please
    audit; same $(cat <<'EOF') shape)

A quick audit: grep -rn '"\$(cat <<' skills/ will enumerate any remaining
instances.

Root cause

Claude Code's permission matcher is conservative by design: any Bash command
containing $(...), backticks, or a pipeline cannot be matched against a
prefix allow rule, because the substituted content could execute arbitrary
code. The safety fallback is a Yes/No prompt, which intentionally has no
"Allow always" button because there is no pattern the harness could confidently
remember.

This is a feature of the matcher, not a bug, so the fix has to happen in the
skill invocation shape rather than in user config.

Proposed fix

Write the body to a temp file first, then pass --body-file. This eliminates
the substitution and makes the gh command a clean prefix match.

Before:

   gh pr create --base dev --title "Foo" --body "$(cat <<'EOF'
   Description.
   Closes #42
   EOF
   )"

After:

   cat > /tmp/cc_pr_body.md <<'EOF'
   Description.
   Closes #42
   EOF
   gh pr create --base dev --title "Foo" --body-file /tmp/cc_pr_body.md

The heredoc still exists, but it now feeds cat > file, which is a simple
prefix match on its own. The gh invocation afterward contains only literal
flags, so Bash(gh:*) matches cleanly and users who have allowed gh get zero
prompts.

Equivalent pattern for gh release create --notes:

   cat > /tmp/cc_release_notes.md <<'EOF'
   ...
   EOF
   gh release create v1.2.3 --notes-file /tmp/cc_release_notes.md

Impact

Before: CodeCannon users running /submit-for-review see 2 Yes/No prompts per
run (PR create + follow-up issue) that cannot be permanently suppressed, no
matter how they configure permissions.

After: The same runs go end-to-end with zero prompts as long as the user has
Bash(gh:*) and Bash(cat > /tmp/*:*) (or similar) allowed. The existing
simple gh calls already match the former; the latter is trivial to add.

Notes

  • Temp file names should be namespaced (e.g. cc_pr_body.md) to avoid colliding
    with other tools writing to /tmp.
  • Consider cleaning them up at the end of the skill for tidiness, though it's
    not required for correctness.
  • Same pattern applies to any other --body "$(...)" / --notes "$(...)" /
    --message "$(...)" uses across CodeCannon skills.

Metadata

Metadata

Labels

qa-passedQA completed, passes

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions