skills/github-issue-from-templates/SKILL.md
Create GitHub issues using data-driven templates. Supports any issue type via configurable template configs. Use when the user asks to create a GitHub ticket, issue, or support ticket, or when they want to add a new issue template.
npx skillsauth add dfitchett/agent-skills github-issue-from-templatesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill creates GitHub issues by dynamically fetching field definitions from GitHub issue templates at runtime. Template metadata (triggers, labels, defaults, formatting rules) is stored in per-template JSON config files. Configs can be stored locally or in a GitHub repository for cross-machine and team sharing. The skill itself contains no hardcoded field definitions.
~/.claude/skills/github-issue-from-templates/
SKILL.md # This file — generic workflow engine
references/
schema.json # JSON Schema for template config files
settings-schema.json # JSON Schema for settings.json
~/.claude/configs/github-issue-from-templates/
settings.json # Storage mode config — created on first run
.local/ # Template configs (local mode only) — user-managed, survives skill updates
*.json
.cache/ # Local cache (auto-managed)
*.json # Cached config files (GitHub mode)
templates/ # Cached issue templates (both modes)
<config-id>.yml|md
<owner>/<repo>/<path>/ # Template configs (GitHub mode) — canonical source
*.json
Why a separate directory? The skill installation directory (
~/.claude/skills/...) is replaced onnpx skills update. Storing template configs in~/.claude/configs/github-issue-from-templates/.local/(local mode) or a GitHub repo (GitHub mode) keeps them safe across updates. Thesettings.jsonfile always lives locally since it tells the skill where to find configs.
Before starting the workflow, verify the gh CLI is installed, authenticated, and has the required OAuth scopes.
Constraint: This skill must never run any
gh authcommand other thangh auth status. Allgh authsubcommands that modify authentication state (login,logout,refresh,setup-git,token,switch) are off-limits. When a scope is missing, display the fix command for the user to run themselves — do not execute it.
Run gh auth status. If the CLI is not installed or not authenticated, notify the user that the gh CLI is required and stop.
Parse the gh auth status output (note: it writes to stderr) and extract the token scopes from the Token scopes: line.
gh auth status 2>&1 | grep -i 'token scopes'
GitHub Enterprise hosts: If the selected template config has
repository.hostnameset (e.g.,va.ghe.com), check scopes for that host instead by prefixing the command withGH_HOST=<hostname>. GHE installations may require different scope names (e.g.,read:projectinstead ofproject) — surface whatever the API error response says is missing rather than assuming. Each host has its own token, so verify scopes for the host being used.
Check for the following scopes:
| Scope | Required | Used for |
|-------|----------|----------|
| repo | Always | Creating issues, reading repository contents (templates, configs) |
| project (github.com) / read:project (GHE) | For project boards | Adding issues to project boards, reading project fields (GraphQL API) |
IMPORTANT: If any required scopes are missing, you MUST stop and prompt the user before continuing the workflow. Do not skip ahead, do not silently note the missing scope — present the options below and wait for the user to respond before proceeding to Step 3.
Collect all missing scopes before prompting. Then present options based on what's missing:
If repo is missing (with or without project): The workflow cannot continue. Present only one option — tell the user to fix their permissions and let you know when done:
The
reposcope is required to create issues and read repository contents. Please run the following command in a separate terminal, then let me know when it's done:gh auth refresh -s repo,project(includes
projectfor project board support)
If only repo is missing (and project is present), adjust the command to gh auth refresh -s repo.
After the user confirms they've run the command, re-run gh auth status to verify the scopes are now present. If still missing, notify the user and stop.
If only project is missing: You MUST present both options and wait for the user's choice before continuing:
The
projectscope is needed to assign issues to project boards. You can either:
- Fix now — Run
gh auth refresh -s projectin a separate terminal, then let me know when it's done.- Skip — Continue without project board support. Created issues will not be added to a project board.
Which would you prefer?
Do not proceed until the user has responded. Then:
gh auth status to verify. If still missing, notify the user and offer both options again.projectScopeAvailable = false flag for this session. Step 2.5 will check this flag and skip project board operations.Read the user's global Claude settings (~/.claude/settings.json) and check whether it contains a PreToolUse hook with "matcher": "Bash" whose command references gh auth and the blocked subcommands (login, logout, refresh, setup-git, switch, token).
If such a hook is already present, skip this step — no suggestion needed.
If no such hook is found, suggest that the user add one for hard enforcement. Display the following as a recommendation — do not write to settings.json directly:
Recommended: You can add a Claude Code hook to your global settings (
~/.claude/settings.json) that blocksgh authcommands other thangh auth status. This prevents any skill or agent from modifying your GitHub auth state. To set this up, run/update-configand ask to add the hook, or add the following to yoursettings.jsonmanually:"hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "cmd=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.command // \"\"'); if echo \"$cmd\" | grep -qE 'gh auth (login|logout|refresh|setup-git|switch|token)'; then echo 'BLOCKED: Only gh auth status is allowed. Run other gh auth commands manually outside of Claude.' >&2; exit 2; fi" } ] } ] }
Only show this suggestion once per session — if the workflow loops back to Tool Detection (e.g., after a retry), skip this step.
All GitHub operations in this skill use the gh CLI exclusively.
Before template selection, resolve where configs are stored.
~/.claude/configs/github-issue-from-templates/settings.json exists.references/settings-schema.json, and resolve the storage mode:
configStorage.type === "local" → configs are in ~/.claude/configs/github-issue-from-templates/.local/configStorage.type === "github" → configs are cached locally in ~/.claude/configs/github-issue-from-templates/.cache/ (canonical source is the configured GitHub repo)configStorage.type === "github"):
~/.claude/configs/github-issue-from-templates/.cache/ does not exist → run initial sync (see Syncing Configs from GitHub) to download all configs into .cache/.cache/ exists → use cached files directly (no network call).local/ or .cache/) for use in Step 1.Ask the user how they want to store their template configs:
Option A — Local storage:
~/.claude/configs/github-issue-from-templates/ and .local/ if they don't existsettings.json:
{
"configStorage": {
"type": "local"
}
}
Option B — GitHub repository storage:
owner, repo, path (default: configs/github-issue-from-templates/), branch (default: main)settings.json:
{
"configStorage": {
"type": "github",
"owner": "<owner>",
"repo": "<repo>",
"path": "configs/github-issue-from-templates/",
"branch": "main"
}
}
github-issue-from-templates-configs (default private)gh repo create <owner>/github-issue-from-templates-configs --private --description "Template configs for github-issue-from-templates skill"README.md at the configured pathsettings.json as abovesettings.json, run the initial sync to populate .cache/ (see Syncing Configs from GitHub)If the user asks to change their storage mode (e.g., from local to GitHub or vice versa):
settings.json to determine the current mode.json config from .local/, commit each to the GitHub repo at the configured path via API, then populate .cache/ from the push responses.json config from .cache/ to .local/. Remove .cache/settings.json with the new storage configuration.json files from ~/.claude/configs/github-issue-from-templates/.local/. If the directory does not exist or contains no config files, offer to create a first template config..json files from ~/.claude/configs/github-issue-from-templates/.cache/, excluding README.md. The cache is populated during Step 0 — no network calls are needed here. If the cache is empty, offer to run a sync or add a first config.triggers.keywords (case-insensitive substring match) and triggers.description.name and description and ask the user to choose.name and description and ask the user to choose.When configStorage.type === "github", use this process to download configs from the remote repo into the local cache at ~/.claude/configs/github-issue-from-templates/.cache/. This runs during initial setup (Step 0) and on manual sync requests.
gh api repos/<owner>/<repo>/contents/<path>?ref=<branch> --jq '.[] | select(.name | endswith(".json")) | select(.name != "settings.json") | .name'
gh api repos/<owner>/<repo>/contents/<path>/<filename>?ref=<branch> --jq '.content' | base64 -d
Write each fetched file to ~/.claude/configs/github-issue-from-templates/.cache/<filename>. Create the .cache/ directory if it doesn't exist.
Parse each fetched file as JSON. Skip files that fail to parse and notify the user (see Error Handling).
For each config that was just synced, optionally fetch the corresponding issue template and save it to .cache/templates/<config.id>.<config.templateSource.format>. Create the .cache/templates/ directory if it doesn't exist. This step is optional — templates will be fetched lazily on first use in Step 2 if skipped here.
Before fetching from GitHub, check the local template cache:
~/.claude/configs/github-issue-from-templates/.cache/templates/<config.id>.<config.templateSource.format>gh CLI, then save the raw content to .cache/templates/<config.id>.<config.templateSource.format>. Create the .cache/templates/ directory if it doesn't exist.gh api repos/<config.repository.owner>/<config.repository.repo>/contents/<config.templateSource.path> --jq '.content' | base64 -d
GitHub Enterprise: If
config.repository.hostnameis set, prefix the command withGH_HOST=<hostname>(e.g.,GH_HOST=va.ghe.com gh api repos/...). Apply this to allghandgh apicalls in this step.
Then parse based on config.templateSource.format:
yml (Form-based templates)Parse the YAML content and extract:
title: field (e.g., "[Issue Type] [Short descriptive title]")labels: arrayassignees: arraybody: array. For each entry:
type: markdown — these are instructional text, not fieldsid — unique field identifiertype — dropdown, input, textareaattributes.label — human-readable field nameattributes.description — help text for the fieldattributes.options — available choices (for dropdownsattributes.placeholder — example/guidance textvalidations.required — whether the field must be filledmd (Frontmatter + markdown templates)Parse the frontmatter (between --- delimiters) and extract:
title: (e.g., '[A11y]: Product - Feature - Request')labels: (may be a string or array)assignees: (may be a string or array)Parse the markdown body to identify:
## headings define major sections- [ ] Item text grouped under a heading or bold label- **Team name:** under a sectionScope gate: If the user chose to skip the project scope in Step 2 (projectScopeAvailable = false), skip this entire step. Set config.projectBoard to null for this session and continue to Step 3.
After selecting a template config, check whether config.projectBoard is defined:
config.projectBoard exists with at least name, number, and nodeId: Proceed — the project and its field defaults will be shown in the preview (Step 5).config.projectBoard is missing or incomplete: Prompt the user:
config.projectBoard to null for this session so the preview omits the project line.Ask the user for:
https://github.com/orgs/ORG/projects/123), extract the owner, owner type (organization or user), and number automatically. Otherwise ask for the number.config.repository.owner if not provided or extracted from URL.organization.Use the GitHub GraphQL API via gh api graphql to fetch the project's node ID, name, and fields in a single query:
GitHub Enterprise: If
config.repository.hostnameis set, prefix this command (and all subsequent project mutations) withGH_HOST=<hostname>.
gh api graphql -f query='
query($owner: String!, $number: Int!) {
<ownerType>(login: $owner) {
projectV2(number: $number) {
id
title
fields(first: 50) {
nodes {
... on ProjectV2Field {
id
name
dataType
}
... on ProjectV2IterationField {
id
name
dataType
configuration {
iterations {
id
title
startDate
}
}
}
... on ProjectV2SingleSelectField {
id
name
dataType
options {
id
name
}
}
}
}
}
}
}
' -f owner='<owner>' -F number=<number>
Replace <ownerType> with organization or user based on the owner type determined in Step A.
From the response, extract:
id → store as projectBoard.nodeIdtitle → store as projectBoard.name (confirm with the user: "I found project [title] — is this correct?")fields.nodes → the list of project fields with their types and optionsPresent the fetched fields to the user and ask which ones should have default values. For each field:
Title, Assignees, Labels, Linked pull requests, Reviewers, Repository, Milestone. These are set via the issue itself, not project field values.For each field where the user provides a default, store it in projectBoard.fieldDefaults keyed by the field's node ID:
{
"<field-node-id>": {
"fieldName": "Status",
"value": {
"display": "Backlog",
"optionId": "<option-node-id>"
}
}
}
display and optionIddisplay and iterationIddisplay (the literal value)Gathering style: Don't present every field one at a time. Instead, list all eligible fields with their types and current options in a single message, and ask the user which ones they'd like to set defaults for. Then gather the values for just those fields.
projectBoard objectReturn a complete projectBoard object matching the schema in references/schema.json:
{
"name": "BMT Team 2 Board",
"number": 123,
"nodeId": "PVT_kwHOABC123",
"url": "https://github.com/orgs/ORG/projects/123",
"owner": "ORG",
"ownerType": "organization",
"fieldDefaults": {
"PVTSSF_field1": {
"fieldName": "Status",
"value": { "display": "Backlog", "optionId": "opt_abc123" }
},
"PVTSSF_field2": {
"fieldName": "Priority",
"value": { "display": "High", "optionId": "opt_def456" }
}
}
}
For each extracted field, apply the following logic in order:
Pre-fill from user request: If the user already provided a value for this field in their initial message, pre-fill it and confirm during the preview step.
Apply defaults: Check config.fieldDefaults[fieldId].value — if present, use as the default. Also check config.defaults for matching keys.
Check skip conditions: Check config.fieldSkipConditions[fieldId] — if an onlyWhen condition exists and is not met, skip this field entirely.
Prompt if needed: If the field is required (validations.required: true) and no value has been determined, prompt the user. Use the field's label as the question and description/placeholder as guidance.
Apply gathering notes: Use config.fieldDefaults[fieldId].gatheringNotes for additional guidance on how to present or gather this field.
Gathering style:
Build the title by substituting placeholders in the title pattern:
config.title.override if present; otherwise use the pattern extracted from the template in Step 2[Issue Type] → the value of the issue-type field, [Short descriptive title] → the summary field value)Render the issue body following the structure from the fetched template:
### Field Label section with the gathered value. Use no response for empty optional sections. Maintain the exact field order from the template.Build the label set:
config.labels.defaultlabels: field) — avoid duplicatesconfig.labels.conditional rules:
keyword: Check if any keyword appears in the user's request (case-insensitive)fieldValue: Check if the specified field has the specified valuefieldTransform: Derive a label from a field value using the specified transform (e.g., lowercase-hyphenate converts "Document Status" to "document-status")notesMerge config.assignees.default with template-level assignees. If config.assignees.promptUser is true, ask the user if they want to assign anyone else.
Present the composed issue to the user for review:
**Title**: [composed title]
**Labels**: label1, label2, label3
**Assignees**: @user1, @user2
**Project**: [project board name] (Status: Backlog, Priority: High, ...) ← only if config.projectBoard is set
**Body**:
[rendered body preview]
config.projectBoard is set, display the project name. If config.projectBoard.fieldDefaults contains entries, list the default field values that will be applied (e.g., "Status: Backlog, Priority: High").config.projectBoard is null or missing, omit the Project line entirely.Ask for confirmation or edits. If the user requests changes, apply them and re-preview.
Create the issue using gh issue create:
gh issue create \
--repo <config.repository.owner>/<config.repository.repo> \
--title "<composed title>" \
--body "<composed body>" \
--label "<label1>" --label "<label2>" \
--assignee "<assignee1>" --assignee "<assignee2>"
--label / --assignee flagconfig.repository.hostname is set, prefix the command with GH_HOST=<hostname> (e.g., GH_HOST=va.ghe.com gh issue create ...). The gh CLI's --repo flag does not switch hosts — only GH_HOST does.gh issue create \
--repo owner/repo \
--title "Title" \
--body "$(cat <<'EOF'
<composed body>
EOF
)" \
--label "label1" --label "label2"
The gh issue create command prints the issue URL to stdout. Always display the issue URL to the user as a clickable link, regardless of whether config.postCreation is configured. This is the minimum required output on success.
If config.postCreation.displayFormat is defined, also render it by substituting {issueNumber} and {issueUrl} with actual values.
Display each item from config.postCreation.additionalNotes as a follow-up note.
When rendering links in the issue body, apply the rules from config.linkFormatting.rules in order. For each link:
match description to determine if it appliesformat specified by the matching rulecustomText as link text if providedCommon patterns:
(see design) as text[descriptive text](url)When config.acceptanceCriteria is defined:
config.acceptanceCriteria.defaultItems as baseline criteriaconfig.acceptanceCriteria.formatting.style (checklist = - [ ] item, bullets = - item)config.acceptanceCriteria.formatting.avoidPrefixesIf settings.json exists but fails validation against references/settings-schema.json:
settings.jsonIf syncing configs from GitHub fails (during initial setup or manual sync):
.cache/ exists with files: Warn the user that the sync failed, but continue using the existing cached configs. Suggest retrying later..cache/ is empty or does not exist: Cannot proceed with GitHub mode. Notify the user and offer to switch to local storage mode.gh auth status to verify authenticationIf the config directory (.local/ or .cache/) exists but contains no .json config files:
If pushing a config to GitHub fails:
.cache/ — confirm this to the userIf a cached template file in .cache/templates/ fails to parse:
If the template fetch fails (gh api) and no valid cache exists:
gh auth status to verify authenticationIf a template config file is malformed:
If gh issue create fails:
gh command for diagnosticsTo add support for a new issue type, create a new .json config file following the schema in references/schema.json. The save location depends on the storage mode configured in settings.json:
configStorage.type === "local").json file in ~/.claude/configs/github-issue-from-templates/.local/references/schema.jsonrepository.owner and repository.repo to the target GitHub repositoryrepository.hostname (e.g., "va.ghe.com"). Omit for github.com.templateSource.path to the repo-relative path of the GitHub issue templatetemplateSource.format to yml or md based on the template typetriggers.keywords for automatic template matchingprojectBoard. If the user declines, omit the projectBoard property.fieldDefaults, fieldSkipConditions, label rules, and formatting overridesconfigStorage.type === "github")Compose the config JSON following references/schema.json
Prompt for a default project board: Run the Project Gathering flow to populate projectBoard. If the user declines, omit the projectBoard property.
Write the file to the local cache at ~/.claude/configs/github-issue-from-templates/.cache/<filename>.json (immediately available for use)
Push to GitHub:
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json \
--method PUT \
--field message="Add <template-name> template config" \
--field branch=<branch> \
--field content=$(echo '<JSON content>' | base64)
If the push fails, the config is still saved locally in .cache/ — warn the user to sync later once the issue is resolved
No changes to this SKILL.md file are needed
Read, edit, and overwrite the .json file in ~/.claude/configs/github-issue-from-templates/.local/ directly.
Edit the file in the local cache at ~/.claude/configs/github-issue-from-templates/.cache/<filename>.json
Fetch the current SHA from GitHub:
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json?ref=<branch> --jq '.sha'
Push the updated file to GitHub with the SHA:
gh api repos/<owner>/<repo>/contents/<path>/<filename>.json \
--method PUT \
--field message="Update <template-name> template config" \
--field branch=<branch> \
--field content=$(echo '<updated JSON>' | base64) \
--field sha=<current SHA>
If the push fails, the local cache already has the update — warn the user to sync later once the issue is resolved
If the user asks to sync configs (or if configs seem stale), re-run the full download flow from Syncing Configs from GitHub. This overwrites the contents of .cache/ with the latest files from the remote repo. Additionally, for each synced config, re-fetch the issue template from GitHub and update .cache/templates/<config.id>.<config.templateSource.format>.
Note: Templates are cached lazily (on first use in Step 2), so a manual sync only refreshes templates for configs that already have a cached template in
.cache/templates/.
Delete the .cache/ directory entirely. The next skill invocation will detect the missing cache and re-download everything during Step 0. This removes both cached configs and cached templates. To only refresh templates, delete .cache/templates/ — templates will be re-fetched lazily on next use.
no response for any section where no information was provided — never omit sectionsdocument-status, bmt-team-2)data-ai
Post a Zoom meeting recap (TLDR, summary link, recording link, password) to a Slack channel via a Slack bot. Designed to be invoked from a cron-scheduled routine that fires ~30 min after a recurring meeting ends. Pulls the most recent Zoom recording, then prompts a configurable Slack target (group of handles or a channel) for any missing details — the recording password if needed, the presenter's Slack handle for crediting, and the TLDR text itself when neither Zoom's AI summary nor the transcript yields a usable one — and posts the assembled recap exactly once. Use when the user says "post the meeting recap to Slack", "publish the Zoom summary", "/zoom-meeting-slack-recap", or wires up a new recurring-meeting routine.
documentation
Generate a pull request description markdown file from the repository's PR template, auto-filled with context from git diff and commit history. Use when the user says "write a PR description", "fill out the PR template", "create a PR description", "draft a PR", "make a PR doc", or asks you to prepare a pull request. Also trigger when the user says "/pr-description" or references preparing changes for review. Works with any repository that has a PR template.
data-ai
Example TaskFlow authoring pattern for inbox triage. Use when messages need different treatment based on intent, with some routes notifying immediately, some waiting on outside answers, and others rolling into a later summary.
data-ai
Example TaskFlow authoring pattern for inbox triage. Use when messages need different treatment based on intent, with some routes notifying immediately, some waiting on outside answers, and others rolling into a later summary.