Agent skill

Create PR

by sd0xdev in sd0xdev/sd0x-harness

Create or update GitHub PR with gh CLI. An agent skill from sd0xdev/sd0x-harness.

MITAuto-check passedDevelopment

Install Create PR

skills CLI
$ npx skills add sd0xdev/sd0x-harness --skill create-pr -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install sd0xdev/sd0x-harness create-pr --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/sd0xdev/sd0x-harness.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/create-pr .claude/skills/create-pr && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
create-pr
GitHub stars
192
Token cost
~12k tokens
SKILL.md length
6,199 words
Files
3 (incl. scripts, references)
Skills in repo
91
Repo updated
First seen
Licence
MIT

At a glance

Create or update GitHub PR with gh CLI. An agent skill from sd0xdev/sd0x-harness.

  • Works in 8 steps: Mode Dispatch (first, before anything… → Gather Info → Extract Ticket ID → …
  • : user asks to open/create/update a PR
  • SKILL.md covers Input, Workflow, Multi-PR Mode and Stacked PR Mode, plus 2 more sections
  • Runs Shell scripts from its folder; calls gh, git and bash

What it does

Create PR is an agent skill from sd0xdev/sd0x-harness. Create or update GitHub PR with gh CLI. Auto-extracts ticket ID from branch name, generates title/summary from commits. Auto-detects existing PR and switches to update mode. Supports --stack for stacked PR chains (per-layer PRs with chained bases; never executes push/rebase). Default: --dry-run (show command, don't execute). Use when: user asks to open/create/update a PR, says /create-pr, wants a stacked PR chain, wants to refresh PR description after new commits, or says 'update pr', 'update PR title', 'refresh…

Its SKILL.md is about 12k tokens, which your agent loads only when the skill is triggered. The skill folder holds 4 other files, including scripts and reference files (for example `references/stack-mode.md` and `scripts/sanitize-pr-content.sh`).

It sits in Development, covering Pull requests. It works with GitHub and Git. The repository describes itself as: The harness layer for Claude Code — a reference implementation of harness engineering with hook-enforced dual review, state-machine gates that survive context compaction, and… The licence is MIT.

When your agent uses it

  • : user asks to open/create/update a PR
  • Says /create-pr
  • Wants a stacked PR chain
  • Wants to refresh PR description after new commits

Example prompts

  • “update pr”
  • “update PR title”
  • “refresh PR body”
  • “/create-pr”

Requirements

  • A Bash shell
  • Pre-approved tools (allowed-tools): Bash(git:*), Bash(gh:*), Bash(mktemp:*), Bash(rm:*), Bash(bash:*), Read, Write, Grep, Glob, AskUserQuestion, Skill

Workflow steps

8 steps, taken from the step headings in SKILL.md.

  1. Mode Dispatch (first, before anything else)
  2. Gather Info
  3. Extract Ticket ID
  4. Generate Title
  5. Generate Body
  6. Pre-flight Checks + Mode Detection
  7. Output (dry-run, default) — Create Mode
  8. Execute (--execute flag)

What it can do on your machine

Read from SKILL.md and the folder at commit c9a2036. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Bash(git:*)
    • Bash(gh:*)
    • Bash(mktemp:*)
    • Bash(rm:*)
    • Bash(bash:*)
    • Read
    • Write
    • Grep
    • Glob
    • AskUserQuestion

    …and 1 more on the same allowed-tools line.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Ships 1 file in scripts/ (Shell), which the agent can run.

    Shell commands in SKILL.md call:

    • gh
    • git
    • bash

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use gh and git, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Create PR loads about 12k tokens when it runs, and up to ~22k if it reads all its reference files. Until then it costs about 134 tokens; SKILL.md has 6,199 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~134
When it runs · the whole SKILL.md, loaded when a task matches
~12k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~22k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.

SKILL.md

The full file from sd0xdev/sd0x-harness at commit c9a2036, republished under its MIT licence (© sd0xdev). 6,199 words, ~11,509 tokens.

Download SKILL.mdSave it as .claude/skills/create-pr/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.
name
create-pr
description
Create or update GitHub PR with gh CLI. Auto-extracts ticket ID from branch name, generates title/summary from commits. Auto-detects existing PR and switches to update mode. Supports --stack for stacked PR chains (per-layer PRs with chained bases; never executes push/rebase). Default: --dry-run (show command, don't execute). Use when: user asks to open/create/update a PR, says /create-pr, wants a stacked PR chain, wants to refresh PR description after new commits, or says 'update pr', 'update PR title', 'refresh PR body'.
allowed-tools
Bash(git:*), Bash(gh:*), Bash(mktemp:*), Bash(rm:*), Bash(bash:*), Read, Write, Grep, Glob, AskUserQuestion, Skill

Create PR

Input

/create-pr [--head <branch>] [--base <branch>] [--title <title>] [--stack <branch...>] [--update] [--execute] [--dry-run]

  • --head: Source branch (default: current branch)
  • --base: Target branch (default: {TARGET_BRANCH} or main)
  • --title: Override auto-generated title (rejected with --stack)
  • --stack: Stacked PR chain mode, bottom layer first (mutually exclusive with --head; the bottom layer's base follows the same --base / {TARGET_BRANCH} / main resolution as normal mode) — see Stacked PR Mode
  • --update: Force update mode (re-generate title/body for existing PR)
  • --dry-run: Show command without executing (default). Scopes to mutating gh calls — the Step 1 git fetch --prune origin still runs, so the preview reflects the server's real refs
  • --execute: Actually create/update the PR (requires user confirmation)
  • No args: use current branch → default target, dry-run mode. Auto-detects existing PR → update mode

Workflow

0. Mode Dispatch (first, before anything else)

When --stack is present: read references/stack-mode.md and run Phases A, B, D0, then either the native delegation (D1) or Phase C — never both. Skip generic Steps 1, 5, 6 and 7 entirely — Phase A must run git fetch --prune origin and classify sync state before any PR planning, so the generic ls-remote / local base..head path must not run first. What is reused, per layer, exactly as Phase C directs:

ReusedSkipped
Steps 2–4 content generation (ticket ID, title, body)Step 1 gather — Phase A replaces it
Step 4b sanitization, Step 7b post-creation verifyStep 5 pre-flight + mode detection — Phase A/B replace it
Step 5a's smart-diff update logic — the routing decision is Phase B's, the diff-and-update mechanics are Step 5a'sSteps 6–7 single-PR output/execute — Phase C emits per-layer commands instead

Otherwise continue with Step 1 below.

1. Gather Info

The fetch runs first and alone — the ref-range reads below it (git log and git diff over refs/remotes/origin/*) depend on the remote-tracking refs it refreshes, so it cannot join the parallel batch. It runs even in --dry-run (the dry-run promise at the top scopes to mutating gh calls; refreshing and pruning local remote-tracking refs is how the preview describes the commits that would actually ship). ls-remote in the next fence only LISTS the server's refs — it never updates refs/remotes/* — so without this fetch they can be missing or stale (another clone pushed, or this one never fetched) and the PR body would describe old commits. Same exact form and same discipline as stack mode's Phase A: the explicit exit keeps a failed fetch from being followed by reads of stale refs.

Emit this line in the turn that runs the fetch, before the fence — rules/git-workflow.md lists status | diff | log | branch | rev-parse as allowed and git fetch is in neither that list nor the forbidden one, which makes it a Default-tier deviation, and a deviation is declared per run, not once during development (same shape as stack-mode.md § Phase A; the stated reason differs because the fetch serves PR-body range generation here, sync classification there):

[DEVIATION] rule=rules/git-workflow.md § allowed ops default=fetch is not in the allowed list chosen=git fetch --prune origin
reason=the PR body is generated from origin/<base>..origin/<head>; without the fetch every range is computed from stale remote-tracking refs signal=fetch is absent from the forbidden closed set (add|commit|push|stash|reset --hard|rebase) and writes only remote-tracking refs — no working tree, no history
bash
git fetch --prune origin || exit "$?"

The rest are independent — run them in parallel:

bash
# Current branch
git rev-parse --abbrev-ref HEAD

# Remote repo (owner/repo)
gh repo view --json nameWithOwner --jq '.nameWithOwner'

# Check if head branch is pushed
git ls-remote --heads origin -- 'feat/PROJ-42-add-widget'

# Check existing PR
gh pr list --head 'feat/PROJ-42-add-widget' --base 'main' --json number,title,state

# Commits between base..head
git log --oneline 'refs/remotes/origin/main..refs/remotes/origin/feat/PROJ-42-add-widget'

# Full diff for summary
git diff 'refs/remotes/origin/main...refs/remotes/origin/feat/PROJ-42-add-widget' --stat
2. Extract Ticket ID

From branch name, extract ticket ID using {TICKET_PATTERN} (default: [A-Z]+-\d+):

Branch PatternTicket ID
fix/PROJ-520PROJ-520
fix/PROJ-520-2PROJ-520
feat/PROJ-123-some-descPROJ-123
refactor/PROJ-999PROJ-999

Regex: first match of {TICKET_PATTERN} — take first match. Strip trailing -N suffixes.

3. Generate Title

Format: <type>: [<TICKET>] <concise summary>

  • <type>: from branch prefix (fix/ → fix, feat/ → feat, docs/ → docs, refactor/ → refactor)
  • <TICKET>: extracted ticket ID (omit if none found)
  • <concise summary>: summarize commits in <60 chars, focus on main changes
4. Generate Body
markdown
## Summary

<3-5 bullet points summarizing changes from commits>

## Ticket

[<TICKET>]({ISSUE_TRACKER_URL}<TICKET>)

## Test plan

- [ ] <test items based on what changed>

Rules:

  • No AI-generated tags — enforced by Step 4b sanitization (see below)
  • Keep summary factual, based on actual commits
  • Use imperative mood in bullet points
  • Omit Ticket section if no ticket ID or {ISSUE_TRACKER_URL} not configured

Forbidden patterns (case-insensitive ERE with \b word boundaries — canonical source: scripts/commit-msg-guard.sh):

Pattern CategoryRegex
Co-Authored-By AICo-Authored-By:.*(Claude|Anthropic|\bAI\b|GPT|OpenAI|Copilot|Codex|Gemini|noreply@anthropic)
Generated-by tagGenerated[ -](by|with).*(Claude|Anthropic|\bAI\b|GPT|OpenAI|Copilot|Codex|Gemini)
Emoji robot tag🤖.*(Claude|Anthropic|\bAI\b|GPT|OpenAI|Copilot|Codex|Gemini)

Note: \| in the table above is Markdown table escaping. Actual ERE uses unescaped |. Only AI is \b-bounded — it prevents bare AI from matching inside ordinary words ("maintainer", "domain") under -i. GPT and OpenAI are intentionally left unbounded so they still match inside ChatGPT / GPT-4 (no English word contains "gpt"). Generated[ -] covers the hyphenated Generated-by: form, which the earlier space-only version accepted.

4b. AI Content Sanitization

After generating title and body (Step 3-4), scan for forbidden patterns and sanitize before any output or execution. Applies to all modes: dry-run/execute, create/update, --title override.

Sanitization is executed, not paraphrased — skills/create-pr/scripts/sanitize-pr-content.sh is the implementation, and it reads the three forbidden patterns out of scripts/commit-msg-guard.sh at runtime so the two can never drift. The path is resolved from the script's own location, after following any symlink chain, and from nothing else — PLUGIN_ROOT is deliberately not consulted, even though run-skill.sh exports it to exactly that value, because an environment variable that selects the policy source lets a caller swap in a guard declaring three never-matching patterns and get exit 0 on a real trailer. What self-location cannot cover is an invocation whose path is not the file's real location — a copy or a hardlink into a planted tree; the script says so in its own comment rather than implying otherwise, and run-skill.sh building an absolute target from its own location — after physically resolving its own symlink chain, without which a symlink to the wrapper planted in an attacker's tree selected that tree's copy of the policy script — is what closes that for the documented entrypoint. The documented invocation spells the interpreter absolutely (/bin/bash -p) for the same reason one layer earlier: a bare bash is resolved in the caller's shell, so an exported bash function answered the whole command with exit 0 and neither script ever started. A pattern set that reads as empty, as fewer entries than the guard declares, or as a line this parser cannot fully read aborts with exit 2 rather than reporting content clean.

The run directory is allocated before this step, in every mode — including dry-run. Sanitization operates on files, so a mode that writes no file cannot sanitize; a dry-run that skipped it would render an unsanitized body into its own report, which is precisely the text a user copies into gh. What dry-run must not do is leave the files behind or run a mutating gh call, and § Command Rendering's teardown fence is what guarantees the first. "No gh at all" would be the wrong contract and the wrong claim: stack mode's Phase B reads gh pr list and Phase D reads gh extension list — both read-only — in order to decide what to print. The line is gh pr create / gh pr edit: those are never run in dry-run. The lifecycle below is one sequence with one owner, and every exit from it — including a sanitizer failure — passes through the same teardown:

#StepOn failure
1Allocate the run directory (mktemp -d, § Command Rendering step 1)Nothing was created; stop
2Write pr-title.txt and pr-body-N.md out of band (Write tool) — stack mode writes one of each per layer (pr-title-N.txt), see references/stack-mode.mdRun the teardown fence, then stop
3title mode on the title fileExit 3 → regenerate once; still 3 → teardown, then hard fail
4body-inplace mode on each body fileExit 2 → teardown, then hard fail
5Operate (gh) — execute mode only; dry-run renders the report instead. The --title value is read back from the file step 3 scanned, not from the generator's copyThe guarded block already cleans and re-raises
6Teardown (§ Command Rendering) — in dry-run this is a step of its own, run after the report is rendered—

Why step 5 reads the title back. --body-file names the very artifact body-inplace rewrote, so nothing in this workflow re-renders the body between the verdict and the send. gh has no --title-file, so the title is the one field this workflow could make diverge by itself: scan pr-title.txt, then render --title from a string the generator still holds, and the verdict belongs to bytes that were never published. Regenerating the title after the scan (step 3's "regenerate once" path) is exactly when that happens. The file is the artifact of record: re-scan it after any regeneration, and render the flag from what it holds.

What this does not establish — stated because the earlier wording overclaimed it. "Same pathname" is not "same bytes". Scan and publish are separate processes reading a mutable path, so between them any process running as the same user — a second agent, a stray editor, anything sharing the account — can replace either file, and the 0700 run directory does not help because it is that same user's own directory. The sanitizer has a narrower instance of the same gap internally: it scans the path, then reopens it to emit. Closing this properly means one hardened operation owning both the verdict and the send (sanitize and pipe those exact bytes into gh --body-file -), which gh's per-flag interface and this skill's agent-driven step sequence do not currently allow. So the honest contract is: this workflow never itself publishes unscanned bytes; it does not defend against a concurrent same-user writer. Step 7b's post-publication scan is what covers that residual, and it is detection — it runs after gh has already sent the content, so it bounds exposure rather than preventing it.

Steps 3 and 4, with <PR_BODY_DIR> replaced by the literal path from step 1:

bash
/bin/bash -p scripts/run-skill.sh create-pr sanitize-pr-content.sh title '<PR_BODY_DIR>/pr-title.txt'
bash
/bin/bash -p scripts/run-skill.sh create-pr sanitize-pr-content.sh body-inplace '<PR_BODY_DIR>/pr-body-1.md'

Title sanitization (regenerate/fail) — title mode exits 3 on a match, never rewrites, and reports [AI_DETECTED] line <n> matched pattern <k>:

  1. Scan title for forbidden patterns
  2. Exit 3 → regenerate title from commits (1 attempt, without AI attribution) and re-run
  3. If the regenerated title still exits 3 → HARD FAIL: tear down, abort with an error message. No gh command runs
  4. --title override: same scan-and-fail logic (no regeneration — user-provided text fails immediately if matched)

Body sanitization (line-strip + log) — body-inplace replaces the file with its sanitized content and logs each removal to stderr:

  1. Scan body line-by-line for forbidden patterns
  2. Remove matching lines
  3. Log each removal as [AI_STRIPPED] line <n> matched pattern <k> — the matching line itself is never echoed. A PR body is attacker-influenced text and a matched line can carry a token (Generated by GPT-4; token=…); @rules/security.md forbids putting one into a log. The line number locates it in the file
  4. If all content lines removed → preserve template structure (Summary / Test plan headers only)

Use body-inplace, not body with a redirect: … body <file> > <file> truncates the file before the sanitizer reads it, so gh would receive an empty body. body mode (stdout) exists for previewing and for tests; body-inplace is what the workflow runs, and it replaces the file through a sibling temp file and an atomic rename. In --stack mode this happens per layer, on that layer's own pr-body-N.md, immediately before that layer's block.

5. Pre-flight Checks + Mode Detection
CheckAction if fails
Head branch not pushedWarn: "branch not pushed to remote, push first" and STOP
PR already exists→ Enter Update Mode (see section below)
--update flag + no existing PRWarn: "no PR found for this branch" and STOP
No commits between base..head (create mode)Warn: "no diff between branches" and STOP
No commits between base..head (update mode)Continue — PR may need title/body refresh from --title override

Mode detection logic:

ConditionMode
--update flag passedForce update mode (error if no PR exists)
Existing PR detected (auto)Update mode (auto-switch)
No existing PR, no --updateCreate mode (original workflow)
5a. Update Mode

When an existing PR is detected (or --update is passed):

Step 1: Fetch current PR state (use PR number from pre-flight gh pr list result):

bash
gh pr view <PR-number> --json number,title,body,url,baseRefName

Step 2: Re-generate title and body from latest commits (same logic as Steps 2-4 above, using full commit range base..head). Run Step 4b AI Content Sanitization on the re-generated content before proceeding.

Step 3: Smart diff — compare current vs newly generated:

FieldCurrentNewAction
TitlesamesameSkip (no change needed)
TitlediffersdiffersShow before/after
BodysamesameSkip
BodydiffersdiffersShow before/after

Step 4: Decision — if both title and body are unchanged → report "PR is already up to date" and STOP. Step 4b has already allocated the run directory and written the title and body files by this point, so STOP means running the teardown fence first (§ Command Rendering, <PRIOR_STATUS> = 0). An early exit is still an exit from the lifecycle, and it is the one most easily mistaken for "nothing happened".

If changes detected, show the diff and decide what to update:

  • Title changed significantly: update title automatically. Criteria: type prefix changed (fix: → feat:) or ticket ID changed.
  • Title changed trivially: AskUserQuestion — "Title changed slightly. Update?" (show before/after). Criteria: only the summary text after <type>: [<TICKET>] differs.
  • Body changed: always update (body reflects commit history, should stay current)
  • When --title is passed: override title regardless of diff

Step 5: Output (respects --dry-run / --execute):

Dry-run (default) — show the gh pr edit command with only changed fields included:

Every update is the canonical block below, instantiated — never a bare gh pr edit. A bare one is unguarded, so a caller's errexit exits at the failing gh before any cleanup runs, and Step 4b has already allocated the run directory and written the title and body files into it by this point. Only the two parameters the Cleanup row names vary, and no copy of the shape is kept here — a second copy is a second source, and this section is where it would drift from:

UpdateFlags on the block's gh pr editCleanup operand
Title only--title with the rendered title, no --body-file<PR_BODY_DIR> — nothing was written for this operation, but the Step 4b directory is still this path's to remove
Body--body-file naming the file in the run directory<PR_BODY_DIR>
Body + titleboth<PR_BODY_DIR>
Command Rendering (mandatory) ⚠️

Branch names, titles and bodies are all attacker-influenceable — a branch name is accepted by git check-ref-format --branch with ;, &, quotes and $( ) in it, and the body is generated from commit messages. Two rules, and both are load-bearing:

1. Single-quote rendering, never double quotes. Every dynamic value interpolated into a rendered command is wrapped in single quotes with embedded quotes escaped:

render(v) = ' + v.replace(every ' with '\'') + '

Double quotes are not a substitute: "$(id)" still runs id, so git rev-parse "refs/heads/$BRANCH" executes a command substitution embedded in a branch name. Templates in this skill and in references/stack-mode.md show values already rendered. Add -- before positional arguments wherever the CLI accepts it, so a value cannot be parsed as a flag.

2. Body text never appears inside shell syntax — no heredoc, ever. This is a prohibition, not a preference, and it has no "unless" clause. A heredoc terminates at the first line equal to its delimiter, so a body containing that line closes the heredoc early and every following line is parsed as shell input — arbitrary command execution, not a formatting bug. Quoting the delimiter (<<'X') only disables expansion inside the body; it does not prevent the collision. Nor does a random-looking fixed delimiter: fixed is fixed, and a body can contain it. --body-file /dev/stdin does not help either — termination happens in the shell before gh runs.

PathRule
RequiredWrite the body to a file out of band — the Write tool when the skill runs (it is in allowed-tools for exactly this), the user's editor when they copy-paste — then pass --body-file '<path>'. The command names the file and never contains the body
WhereA directory allocated by mktemp -d, one per run: mktemp -d is atomic, returns a unique name, and creates it 0700. Never invent the name — an invented path is not created, so Write fails on the missing parent, and on a shared /tmp a predictable name can be pre-created or symlinked by another user. Never under .git/ either: pre-edit-guard.sh rejects every .git/ path, and in a linked worktree .git is a file, not a directory
How the path is carriedRun mktemp -d once, read the path it prints, and substitute that literal absolute path into every later command. Each Bash invocation is a fresh shell, so DIR=$(mktemp -d) followed by $DIR in a later step silently resolves to nothing — and on macOS TMPDIR is an ambient variable pointing at the shared temp root, so a stray rm -rf "$TMPDIR" would target that root. Same rule and the same reasoning as skills/necessity-audit/SKILL.md § Phase 0. <PR_BODY_DIR> below marks where the returned literal goes
CleanupThe shell shape is defined once, by the canonical block below, and every body-file command in this skill — create, update, stacked, Step 7b — uses that shape unchanged. What varies between them is the operation and the cleanup operand, and only those: a single-PR command cleans the whole run directory, while a stacked layer cleans its own pr-body-N.md and leaves the directory for the layers above it (references/stack-mode.md § Phase C). The structure around them — subshell, seeded status, guarded operation, guarded cleanup, arithmetic re-raise — is copied verbatim. Do not restate it in prose; a restatement is a second source that can drift. What the shape buys, so it is not "simplified" away: the operation is guarded so a caller's errexit cannot exit at the failing gh before cleanup runs (an unguarded command followed by a capture line is skipped outright); the status is carried in a subshell's positional parameters rather than a named variable, because a named one belongs to the caller — readonly STATUS=9 would make the seeding assignment itself fail after allocation, leaking the directory, and an ordinary caller would silently lose its own value; the expansion is quoted, because a bare one is field-split with the caller's IFS and an IFS containing the status digit drops the status (bash and sh report a different code, zsh under SH_WORD_SPLIT reports success); and the subshell keeps the re-raise from closing an interactive shell. Cleanup runs on success and on failure — a PR body can carry private repository context even without credentials — but it must never mask the failure: an unconditional trailing rm succeeds, so the block would report 0 after a failed gh. Capture, clean, re-raise — and the cleanup is guarded in turn, because a failing rm would otherwise replace the operation's status with its own. -- guards the operand, and the operand is the exact literal mktemp -d printed: an empty one deletes nothing, while a wrong non-empty literal would delete the wrong directory — which is why the exact-output provenance rule above is load-bearing. Bash(mktemp:*) and Bash(rm:*) are in allowed-tools for exactly these two steps, and Bash(bash:*) for the Step 4b/7b sanitizer invocation
NeverAny << heredoc, echo/printf of body text, or body interpolated into a command string. With no delimiter in play, no body line can collide with one
Commands this skill executesPass arguments as an array — never interpolate body or title into a shell string

This applies to every mode: create, update, stacked, and the Step 7b remediation below.

Canonical cleanup block. Every body-file command below is this block with its own gh invocation substituted — the one authority for the shape. It is two fences, and the split is load-bearing: a single fence containing both the allocator and the placeholder has no correct way to be run. Execute it whole and mktemp -d's output is discarded while gh is handed the un-substituted literal <PR_BODY_DIR>/…, which does not exist; run the allocator separately and then execute the same fence "verbatim" and it allocates a second directory that nothing ever removes. A shell comment cannot pause execution while a body is written out of band — only a fence boundary can.

bash
# Step 1 — allocate the run directory. Run this fence alone and read the literal
# path it prints. Nothing else belongs in it.
mktemp -d

Step 2 is out of band and is not shell: write each body file into that directory with the Write tool (or your editor). If that write fails or is interrupted, the directory is already allocated and may already hold a private body, so the teardown fence is the immediate next action rather than step 3.

The same fence is the single teardown for every exit that does not reach step 3 — a failed or interrupted write, a Step 4b sanitizer failure (exit 2 or a title that fails twice), a user declining the confirmation, and the end of a dry-run preview. Step 3's own guarded block covers the paths that do reach it. Between them, no route out of the lifecycle leaves the directory behind, which is what makes "the body never outlives the run" a property rather than an intention:

bash
# Any exit before step 3 — remove the allocated directory before anything else.
(
  set -- '<PRIOR_STATUS>'
  case "$1" in ''|*[!0-9]*) set -- 2 ;; esac
  rm -rf -- '<PR_BODY_DIR>' || set -- "$1" "$?"
  exit "$(( $1 ? $1 : ${2:-0} ))"
)

<PRIOR_STATUS> is the status the run already carries, and it is single-quote rendered like every other dynamic value — there is no bare-placeholder exemption in this skill. It once had one, on the reasoning that an arithmetic operand must not be quoted; that was wrong in both directions. $(( … )) converts a quoted digit string without complaint, and an unquoted set -- <PRIOR_STATUS> executes a hostile value at substitution time, before any arithmetic runs — the exemption created the hole it was reasoned into. The case line is the second layer: $(( $1 ? … )) re-evaluates $1's contents as an expression, so a value like a[$(…)] would still run a command substitution there; anything that is not a run of digits becomes 2 (environment failure) before the arithmetic sees it. Cleanup runs either way.

The value itself: for an out-of-band write failure and for the end of a dry-run it renders as 0 — neither left a shell status behind, so the teardown's own status is the one worth reporting. After a Step 4b failure it renders as the sanitizer's exit (2 for an environment failure, 3 for a title that failed twice), so the reason the run stopped survives the cleanup instead of being replaced by it. In --execute mode it renders as the failing layer's status instead, and cleanup can then only add a failure, never replace one — see references/stack-mode.md § Phase C.

bash
# Step 3 — the guarded operation. Run it only once every <PR_BODY_DIR> is the
# literal path from step 1 and every named body file exists. No allocator here.
(
  set -- 0
  gh pr edit 42 --body-file '<PR_BODY_DIR>/pr-body-1.md' || set -- "$?"
  rm -rf -- '<PR_BODY_DIR>' || set -- "$1" "$?"
  exit "$(( $1 ? $1 : ${2:-0} ))"
)

Why cleanup is guarded too. An unguarded rm masks the operation twice over. It masks a failure when it succeeds — the historical bug — and it masks the operation's status when it fails: under an inherited set -e, gh exiting 37 followed by rm exiting 5 reports 5 in bash, sh, zsh and dash alike. The arithmetic re-raise gives the operation precedence and lets a cleanup failure surface only when the operation succeeded, so a leaked body directory is never silent either.

Execute (--execute) — ask user for confirmation via AskUserQuestion, then run gh pr edit. Output:

PR updated: <URL>
Title: <old-title> → <new-title>
Changes: title updated, body updated
Show full SKILL.md (2,528 more words)Show less
6. Output (dry-run, default) — Create Mode

Show the full gh pr create command. Dry-run runs no mutating gh call (gh pr create / gh pr edit) and leaves nothing on disk — read-only queries such as gh pr list and gh extension list still run, because the report's content depends on their answers — but it does allocate, write and sanitize, because Step 4b operates on files and a preview whose body was never sanitized is exactly the text a user copies into a real gh invocation. The distinction that matters is durability, not activity: the run directory dry-run creates is torn down by the skill before the report is delivered, using the teardown fence below, so no private PR body survives a preview. The gh fence is printed, never run.

The steps below are what dry-run renders for the user to copy. The skill's own dry-run pass runs steps 1–2 plus Step 4b against a directory it allocates and then removes; it stops short of step 3.

bash
# Step 1 — allocate. This fence holds nothing else; read the path it prints.
mktemp -d

Step 2 is out of band and is not shell: write the body into that directory as pr-body-1.md with the Write tool (or any editor). Only then, with <PR_BODY_DIR> replaced by the literal step-1 path:

bash
# Step 3 — the guarded operation. Nothing of the body appears in it.
(
  set -- 0
  gh pr create \
    --head 'feat/PROJ-42-add-widget' \
    --base 'main' \
    --title 'feat: [PROJ-42] Add widget endpoint' \
    --body-file '<PR_BODY_DIR>/pr-body-1.md' || set -- "$?"
  rm -rf -- '<PR_BODY_DIR>' || set -- "$1" "$?"
  exit "$(( $1 ? $1 : ${2:-0} ))"
)

The three steps are run in order, not pasted as one block, and step 3 needs one substitution: mktemp -d prints a path that does not exist until the user runs it, so <PR_BODY_DIR> in the write and in the command is replaced with what step 1 printed. The skill's own preview directory is not reusable for this — it is torn down before the report is delivered, precisely so a private body does not outlive the preview. Say this in the report rather than implying a single paste; the alternative is a user pasting a command with a literal <PR_BODY_DIR> in it. Re-running with --execute avoids the substitution entirely: the skill then performs the allocation, the write and the operation itself, and owns the cleanup.

7. Execute (--execute flag)

Ask user for confirmation, then run the command. Output:

PR created: <URL>
Title: <title>
Base: <base> ← Head: <head>
7b. Post-creation Verify (execute-only)

After gh pr create or gh pr edit completes in --execute mode, verify the published content for AI attribution leaks.

Verification runs twice — once on what was just published, and once more after a remediation attempt. Both runs are the same four-step cycle, defined here once and invoked by reference; writing it out a second time is how the second verification previously degraded into the prose "re-verify via gh pr view", which names no file for the sanitizer to read and therefore produces no verdict at all.

The verification cycle

It needs a directory of its own, and for two different reasons depending on the mode. In single-PR mode the Step 4b run directory is already gone by the time verification starts — step 3's guarded block removes it in the same command that ran gh. In stack mode it is still open: each layer's block removes only its own pr-body-N.md, and the directory itself comes down in a teardown fence after the last layer (references/stack-mode.md § Phase C), while Step 7b runs per layer between those. Either way it must not be reused — a private body must not outlive its operation so a later step can borrow the path, and in stack mode the directory still holds the bodies of the layers not yet published. <PR_BODY_DIR> below is this cycle's own fresh literal, as in the 3a remediation.

StepWhat it doesOn failure
V1Allocate — run the mktemp -d fence alone, read the literal path it printsNothing was created; stop
V2Capture the published content into itRun V4 immediately with <PRIOR_STATUS> = gh's exit, then stop
V3Scan the captured file (scan mode, exit 4 on a leak)Its status is the verdict V4 carries
V4Tear down, whatever V3 reported—

V2's redirect is what makes the scan possible: the sanitizer reads files, so a gh pr view printing only to the terminal leaves V3 scanning a path that does not exist (exit 2 — a fail-closed error, not a verdict). Writing to a different file than any input keeps the body-inplace truncation hazard from arising here. And V2's failure path is not optional: the redirect opens published.txt before gh runs, so a failed gh pr view can still leave a partial snapshot of a private repository's title and body in a directory that has no verdict to reach — only a directory to remove.

bash
mktemp -d
bash
gh pr view 42 --json title,body --template '{{.title}}{{"\n"}}{{.body}}' > '<PR_BODY_DIR>/published.txt'
bash
/bin/bash -p scripts/run-skill.sh create-pr sanitize-pr-content.sh scan '<PR_BODY_DIR>/published.txt'

V4's <PRIOR_STATUS> renders as the status the cycle already carries (0 clean, 4 leak, 2 fail-closed, or gh's own status when V2 failed), so cleanup cannot mask the verdict and the captured snapshot never outlives the check:

bash
(
  set -- '<PRIOR_STATUS>'
  case "$1" in ''|*[!0-9]*) set -- 2 ;; esac
  rm -rf -- '<PR_BODY_DIR>' || set -- "$1" "$?"
  exit "$(( $1 ? $1 : ${2:-0} ))"
)

Step 1–2 — run the verification cycle (V1–V4) against the PR just created or edited. Its result is the verdict: 0 clean and this section is done, 4 a leak, anything else a fail-closed error that stops the run.

Step 3: If leak detected — auto-remediate (single attempt, using pre-sanitized snapshot from Step 4b):

The Step 4b snapshot is held by the skill, not by a shell variable — a variable set in an earlier command does not survive into this one. Re-materialize it here with the same three steps as § Command Rendering — allocate, write out of band, operate — sub-numbered so they are not read as this section's own Steps 1–4:

3a — allocate. Run this fence alone and read the literal path it prints:

bash
mktemp -d

3b — write the snapshot, out of band. This step is not shell: write the Step 4b sanitized body into the directory from 3a as pr-body-1.md, using the Write tool. Skipping it is the one way this remediation silently does the wrong thing — gh is handed a --body-file that does not exist, and the remediation fails after the leak has already been published. If the write fails, run the teardown fence from § Command Rendering and stop.

3c — the guarded operation, with <PR_BODY_DIR> replaced by the literal path from 3a:

bash
(
  set -- 0
  gh pr edit 42 --title 'feat: [PROJ-42] Add widget endpoint' --body-file '<PR_BODY_DIR>/pr-body-1.md' || set -- "$?"
  rm -rf -- '<PR_BODY_DIR>' || set -- "$1" "$?"
  exit "$(( $1 ? $1 : ${2:-0} ))"
)

Step 4 — run the verification cycle again (V1–V4, a fresh directory each time), now against the remediated PR. This is the same sequence as Step 1–2, not a lighter check: the remediation published new content, so the only thing that establishes it is clean is capturing and scanning what GitHub now holds. A 0 here closes the section. If it still reports 4 → HARD FAIL:

❌ AI attribution leaked in PR #<number> after remediation attempt.
   Manual fix: re-run the canonical guarded block (§ Command Rendering) with
   `gh pr edit <number>` as its operation and a freshly sanitized body file.

This message names the canonical block rather than spelling a command out. A second rendering of the same operation is a second thing to keep correct, and this one sat in an untyped fence — outside the reach of the sweep that checks every bash fence — while carrying an unquoted <clean-body-file>.

Guardrails:

  1. Single remediation attempt only — no retry loop
  2. Use pre-sanitized snapshot (do not re-generate from commits)
  3. Fail-fast on GitHub API errors (no retry for transient errors)

Multi-PR Mode

When user specifies multiple branch pairs (e.g. "A → main, B → A"), create them sequentially and output all URLs at the end.

Stacked PR Mode

--stack <branch...> (bottom layer first) turns a linear branch chain into one PR per layer, each based on the layer below. Full detail — phase tables, chain model, shell-safety contract, native gh stack comparison — lives in references/stack-mode.md; what follows is the contract this skill will not deviate from.

Never executes git push, git rebase, or any gh stack subcommand. Under --execute only gh pr create / gh pr edit run, which is this skill's existing authorization (Step 5a, Steps 6-7). Branch pushes go to /push-ci; the native stacked path goes to /gh-stack (Anchor Register #4's fourth workflow), which re-asks for its own install and per-use approvals — delegating is not executing, and no gate is inherited across the call. Everything else is printed for the user to run.

PhaseDoes
A. Sync classificationgit fetch --prune origin, then classify each layer by local/remote OID: IN_SYNC / LOCAL_AHEAD / ABSENT / REMOTE_AHEAD / DIVERGED / NO_SUCH_BRANCH. Every failure in the fence exits explicitly (|| exit), so a failed fetch cannot be followed by probes reading stale refs — set -e would not do it, because a caller that tests the fence's status disables errexit inside it — in every POSIX shell; each ref probe accepts exit 1 and exits on anything else, because absence is an expected answer, and the probes' output is delimited by local: / remote: / end: markers — without them a missing ref prints nothing and ABSENT reads identically to remote-only, whose disposition is the opposite. Runs first — later phases read remote refs, so a missing origin/<head> must surface here
B. Chain validationReal ancestry — every layer on its own base, bottom layer included (git merge-base --is-ancestor origin/<base> origin/<head>) plus each adjacent pair; non-empty layer; existing-PR policy via gh pr list --head <head> --state all --limit 100 (default is open-only, and the default page is 30 — both flags are mandatory); layer-count rules
C. Per-layer create/editBottom to top; existing PR → update (Step 5a), absent → create. Steps 2-4 + 4b per layer; Step 7b per layer under --execute. Execute mode is one guarded block per layer, not one && chain — an upper body carries the lower PR's number, which does not exist until the lower layer has run
D. Environment detection and native routing — D0 runs before Phase CD0: gh extension list, matched on the github/gh-stack identity. D1: --update → Phase C always (the native path has no text-refresh form; delegating would answer a description refresh with a push that refreshes no text). Otherwise present and not excluded by § Phase D's routing table (which sends a chain with any remote-only layer to Phase C, because /gh-stack Phase 2 aborts on a branch with no local ref) → skip Phase C and delegate the validated chain, carrying this run's mode — dry-run sends the read-only /gh-stack --base '<resolved target branch>' <layer>… (the base travels in both modes, or the preview describes a different chain), --execute sends /gh-stack --link --open --base '<resolved target branch>' <layer>… (--link because it is the form that takes operands; --open because the native path otherwise drafts while Phase C does not; --base because dropping it chains layer 1 onto the repository default rather than the branch Phases A/B validated against); absent → under --execute offer the install (that skill's own AskUserQuestion), in dry-run offer nothing and report it — a preview leaves no software on disk; unreadable, declined or failed → Phase C. After a native attempt, the exit and /gh-stack's Phase 4 verification decide together: exit 0 with confirmed and every approved PR change read back is done, while a read-back that differs is partial and stops; a non-zero exit with confirmed stops (the approved command failed against an existing Stack); a non-zero exit with confirmed absent falls to Phase C after re-running Phase B's existing-PR query, since the pre-native snapshot does not know which layers the attempt already published; unverifiable stops without Phase C, because a Stack GitHub did not confirm may still exist. A repository without Stacks never reaches execution: /gh-stack's availability probe stops it first, which is a Phase 2 STOP and falls back from there. Phase C and the native path are alternatives, never a sequence — running both would double-mutate every layer. Native yields a GitHub Stack object (per-layer diff view, linked merges), Multi-PR mode yields chained bases only — the report says which happened, never treating them as equivalent
Sync statedry-run--execute
IN_SYNCcontinuecontinue
LOCAL_AHEADcontinue with stale-content warningrefuse
ABSENTstop before PR planning, emit push remediationrefuse
REMOTE_AHEAD / DIVERGEDabort layer — user resolves via fetch/rebase (push is not the remedy)refuse
NO_SUCH_BRANCHabort — branch exists nowhere; pushing cannot fix a typorefuse
ContractWhat it means
ClassificationAncestry in both directions (merge-base --is-ancestor each way) — OID equality alone cannot tell LOCAL_AHEAD from REMOTE_AHEAD from DIVERGED. Refs fully qualified (refs/heads/…, refs/remotes/origin/…); any fetch or resolution error fails closed
--updateAsserts every layer already has an OPEN PR: if any is absent, Phase B aborts before Phase C — --update never creates. Plain re-entry (no --update) keeps the auto-detected update/create routing
Dependency marker#<N> whenever the lower PR number is known (including in dry-run), a `branch` marker only when that PR is absent, upgraded to #<N> on --stack --update. Never emit an unresolved placeholder
Fail-fast, not atomicLayers are independent mutations, so partial success is a real outcome. On failure, stop before the next layer and report every layer as succeeded / failed / pending; re-running detects created layers in Phase B and routes them to update mode
Shell safetySingle-quote rendering for every dynamic value (double quotes do not suppress $( )), -- as option terminator, and the body never enters shell syntax at all — no heredoc in any form; it is written to a file out of band and passed via --body-file
RejectionsNon-linear chain, layer with no unique commits, PR that is CLOSED / MERGED / base-mismatched / multiply matched, single-layer chain (use plain /create-pr), explicit-but-empty chain, and auto-detection with no authoritative source (existing PR base relations — a tracked native stack is not a source here, § Phase B). A dirty working tree only warns — all content derives from fetched remote refs

Edge Cases

CaseBehavior
No ticket ID in branch nameOmit [TICKET] from title, omit Ticket section from body
Branch suffix like -2, -3Strip suffix when extracting ticket ID
User provides --titleUse as-is (skip auto-generation), but still run Step 4b scan — fail immediately if forbidden pattern matched
Stacked PRs (B → A → main)Prefer --stack (per-layer PRs, chained bases). Without it: note dependency in body — Stacked on #<PR-number> when that PR exists, otherwise the lower branch name
--update but no existing PRError: "No PR found for branch <head> → <base>"
Auto-detect existing PRSwitch to update mode, show "Existing PR #N detected, switching to update mode"
PR body has manual editsRe-generate from commits; user reviews before/after diff
Title unchanged after new commitsSkip title update, only update body

Verification

Create and update modes
  • Step 4b: Title and body pass the forbidden-pattern scan before they are output or sent
  • Step 7b: Post-creation / post-edit verify finds no AI attribution (execute-only)
  • Dry-run commands are valid and runnable in order, with <PR_BODY_DIR> the only substitution the user makes
  • (create) Branch exists and is pushed to remote; no existing PR for the same head/base
  • (create) Title follows project convention; body includes summary and test plan
  • (update) Existing PR fetched successfully (gh pr view); new title/body generated from latest commits
  • (update) Before/after diff shown to user; only changed fields included in gh pr edit
Stacked mode (--stack)
  • No git push, git rebase, or gh stack subcommand executed here — the native path was delegated to /gh-stack, which ran its own gates
  • Phase D reported which path produced the result — native Stack object, or chained-base Multi-PR — and each layer's readiness (the native path drafts unless --open was delegated)
  • Phase A ran first: git fetch --prune origin + per-layer sync classification; ABSENT stopped before PR planning
  • Phase B validated real ancestry and queried PRs with --state all
  • (Multi-PR path) Dependency markers resolved (#N when known, branch marker only when absent) — the native path has no markers of its own: gh stack link chains the PRs and GitHub renders the stack
  • Failure reported per layer (succeeded / failed / pending); re-run created no duplicates
  • (Multi-PR path) Displayed commands single-quote rendered; no heredoc anywhere, body passed via --body-file — on the native path the extension authors the bodies and no --body-file is used

© sd0xdev, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 2 other files (scripts, references) in skills/create-pr of sd0xdev/sd0x-harness.

  • SKILL.md
  • references/stack-mode.md
  • scripts/sanitize-pr-content.sh

Open the folder on GitHubat commit c9a2036

Compare with similar skills

Create PR next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Create PR compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Create PR this skillsd0xdev/sd0x-harness192—~12kAutomated safety check: PassMIT
Contributor-First PR MergeHKUDS/OpenHarness16k1 repos~847Automated safety check: PassMIT
Create Pull Requestcline/cline70k1 repos~1.6kAutomated safety check: PassApache-2.0
Pull Request Title and Body Writeropeninterpreter/openinterpreter69k2 repos~1.1kAutomated safety check: PassApache-2.0
PR Review State Fetchprisma/orm48k—~767Automated safety check: PassApache-2.0
Creating Description For Gh PRredis/jedis12k—~838Automated safety check: PassMIT

Similar skills

  • Merges external GitHub pull requests while keeping the original author credited, and fixes conflicts after the merge instead of rewriting the contribution.

    16k GitHub starsUsed in 1 repo~847 tokens
    DevelopmentAuto-check passed
  • Opens a GitHub pull request from your current branch with the gh CLI, after reviewing the commits and diff and gathering the details the PR needs.

    70k GitHub starsUsed in 1 repo~1.6k tokens
    DevelopmentAuto-check passed
  • Pull Request Title and Body Writer

    openinterpreter/openinterpreter

    Rewrites the title and body of one or more pull requests with gh, leading with why the change was made, then what changed, and describing only the net result.

    69k GitHub starsUsed in 2 repos~1.1k tokens
    DevelopmentAuto-check passed
  • Official

    Fetches a pull request's canonical review state as JSON, validates it, and renders markdown, a text summary and triage target files from it using bundled scripts.

    48k GitHub stars~767 tokensUpdated yesterday
    DevelopmentAuto-check passed
  • Official

    Generate a clear, concise GitHub PR title and description from the diff between two local git branches, and save it to prDescription.md in the repo root.

    12k GitHub stars~838 tokensUpdated today
    DevelopmentAuto-check passed
  • Opens a pull request for the current branch using the repo's template, a work item ID in the title and a description filled in from the actual diff.

    60k GitHub stars~824 tokensUpdated yesterday
    DevelopmentAuto-check passed

More from sd0xdev/sd0x-harness

All 91 skills in this repo
  • Adr

    sd0xdev/sd0x-harness

    Write an Architecture Decision Record (ADR) for a feature — Context / Decision / Status / Consequences / Alternatives, filed as docs/features/<feature/adr-<NNN-<title.md with a 3-digit zero-padded…

    192 GitHub stars~4.8k tokensUpdated yesterday
    Auto-check passed
  • Load PR Review

    sd0xdev/sd0x-harness

    Load GitHub PR review comments into AI session — analyze, triage, plan.

    192 GitHub stars~4.4k tokensUpdated yesterday
    Auto-check passed
  • Next Step

    sd0xdev/sd0x-harness

    Change-aware next step advisor. An agent skill from sd0xdev/sd0x-harness.

    192 GitHub stars~1.6k tokensUpdated yesterday
    Auto-check passed
  • Obsidian CLI

    sd0xdev/sd0x-harness

    Obsidian vault integration via official CLI. An agent skill from sd0xdev/sd0x-harness.

    192 GitHub stars~1.1k tokensUpdated yesterday
    Auto-check passed
  • Orchestrate

    sd0xdev/sd0x-harness

    Agent-driven workflow orchestration (v1 report-only). An agent skill from sd0xdev/sd0x-harness.

    192 GitHub stars~2.5k tokensUpdated yesterday
    Auto-check passed
  • PR Comment

    sd0xdev/sd0x-harness

    Post friendly review comments to a GitHub PR — prepare locally, preview, then submit as atomic review.

    192 GitHub stars~1.5k tokensUpdated yesterday
    Auto-check passed

Works with

Categories

Questions about Create PR

What does Create PR do?

Create or update GitHub PR with gh CLI. An agent skill from sd0xdev/sd0x-harness. Create PR is an agent skill from sd0xdev/sd0x-harness. Create or update GitHub PR with gh CLI.

When should I use Create PR?

Create PR fits situations like: : user asks to open/create/update a PR; says /create-pr; wants a stacked PR chain; wants to refresh PR description after new commits.

How do I install Create PR in Claude Code?

Run `npx skills add sd0xdev/sd0x-harness --skill create-pr -a claude-code`. Or copy the skill folder (skills/create-pr in sd0xdev/sd0x-harness) into .claude/skills/create-pr in your project. Claude Code loads it when a task matches its description.

How do I install Create PR in Codex?

Run `npx skills add sd0xdev/sd0x-harness --skill create-pr -a codex`. Or copy the skill folder (skills/create-pr in sd0xdev/sd0x-harness) into .agents/skills/create-pr in your project. Codex loads it when a task matches its description.

Can I use Create PR in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add sd0xdev/sd0x-harness --skill create-pr -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/create-pr, .gemini/skills/create-pr, .github/skills/create-pr and .opencode/skills/create-pr in your project.

What does Create PR need to run?

Going by SKILL.md and its folder, Create PR needs a shell for the scripts in its folder and the command-line tools its instructions call (gh, git and bash). Our summary lists: A Bash shell. Its frontmatter pre-approves these tools: Bash(git:*), Bash(gh:*), Bash(mktemp:*), Bash(rm:*), Bash(bash:*), Read, Write, Grep, Glob, AskUserQuestion, Skill.

Does Create PR access the network?

SKILL.md contains no URLs. Its commands use gh and git, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Create PR safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.

What licence does Create PR use?

Create PR is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Create PR use?

About 12k tokens (SKILL.md is roughly 46k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 11k tokens, read only when the agent opens those files.

What are the alternatives to Create PR?

Skills that share tags, products or a category with Create PR: Contributor-First PR Merge (HKUDS/OpenHarness, 16k stars), Create Pull Request (cline/cline, 70k stars), Pull Request Title and Body Writer (openinterpreter/openinterpreter, 69k stars) and PR Review State Fetch (prisma/orm, 48k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Create PR?

sd0xdev (a GitHub user) maintains it in sd0xdev/sd0x-harness, which has 192 GitHub stars. The repository holds 91 skills in this directory. The repository was last updated on October 6, 2026.

Source: sd0xdev/sd0x-harness on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.