han-release — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited han-release (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
which gh || echo MISSINGwhich jq || echo MISSINGgit rev-parse --is-inside-work-tree 2>/dev/null || echo NOIf `gh` or `jq` is MISSING, or this is not a git repo: tell the operator which prerequisite is missing and that it must be installed/configured before /han-release can run, then immediately stop. The skill cannot proceed without all three.
gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || git config --get remote.origin.urlgit branch --show-currentgit symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || echo unknowngit status --porcelainjq -r .name .claude-plugin/marketplace.json 2>/dev/nulljq -r '.plugins[] | "\(.name)\t\(.source)\t\(.version)"' .claude-plugin/marketplace.json 2>/dev/nullgit fetch --tags --quiet >/dev/null 2>&1; git tag -l 'v*.*.*' --sort=-v:refname | head -n1grep -m1 '^## v' CHANGELOG.md 2>/dev/nullname (parent plugin name above, normally han). It has no skills or agents of its own; it exists to install the children via dependencies. The git tag tracks the parent's version, so the release tag is v{parent target}.marketplace.json.plugins[] (han-core, han-github, han-reporting, and any future han-* plugin). Each child has its own version line, bumped independently of the others.prev (the latest release tag). For the parent this is prev#. For a child it is the version recorded in that child's plugin.json at prev; if the child did not exist at prev, it is a new plugin (see Step 3).plugin.json.v{parent target}.prev is the latest release tag (for example v2.7.0; the number without the leading v is prev#). On the first release prev is empty.source field in marketplace.json (for example ./han-core), so its plugin.json is {source}/.claude-plugin/plugin.json. Use {source} verbatim in every git command: the ./-prefixed form works both after a {ref}: colon (git show {prev}:{source}/...) and as a pathspec (git diff ... -- {source}/). Do not strip the leading ./.pause_before_publish — true if the argument contains "pause", "review", or "confirm before publish" (case-insensitive). Default false.draft_release — true if the argument contains "draft". Default false.$release_context, passed into the narrative dispatch in Step 5. May be empty.working tree from Project Context is non-empty, there are uncommitted or untracked changes. Stop and tell the operator to commit or stash them first. Releasing an unknown working state is unsafe and a pushed tag is hard to reverse. This is a hard stop, not a pause gate.current branch is not the default branch, do not stop — note in the Step 7 summary that the release is being cut from current branch and the tag will point at that branch's HEAD. The operator chose autonomous; surface the fact, do not block.latest release tag. If it is empty, this is the first release: there is no previous tag, the commit range is the full history, all compare links are omitted, and every plugin is treated as new (Step 3).${prev}..HEAD. First release: the full history (HEAD with no range base).git log {range} --oneline. If it is empty, there are no commits since prev. Stop and tell the operator there is nothing to release. git log {range} --pretty=%s%x00%b | grep -oE '#[0-9]+' | tr -d '#' | sort -unFor each number N, run gh pr view N --json number,title,author,url,mergedAt,state. Keep only entries where state is MERGED. Sort the survivors by mergedAt ascending (newest merge last). This is $pr_list. The PR list is repo-wide and appears once per release; it is not split per plugin. Build the PR lines and the changelog bullets per references/release-notes-format.md and references/changelog-rules.md.
$pr_list is empty (local-only or squash history with no PR refs), record the notable commit subjects from git log {range} --oneline instead, and use the commits form documented in both reference files.N in $pr_list, find the issues that PR closed and credit everyone involved. This relates each closed issue to the fix that resolved it.gh pr view N --json closingIssuesReferences --jq '[.closingIssuesReferences[]?.number]' (the GitHub-tracked closing links). As a fallback for older PRs that linked via text, also scan the PR body and commit messages for GitHub closing keywords: gh pr view N --json body,commits --jq '[.body, (.commits[].messageBody)] | join("\n")' and extract #<num> that follow close, closes, closed, fix, fixes, fixed, resolve, resolves, or resolved (case-insensitive). Union the two sets, dedupe.I, run gh issue view I --json number,title,author,state,comments (suppress stderr; redirect 2>/dev/null). Skip the number if the command fails (it is a PR number, not an issue, or does not exist)..author.login, unless .author.is_bot is true..comments[]), so reaction-only participants are already excluded. Drive-by comments do not count either. Pull each comment with its author and body (gh issue view I --json comments --jq '.comments[] | select(.author.is_bot|not) | {login: .author.login, body: .body}', stderr suppressed), and treat a comment as a drive-by when its trimmed body is emoji-only, or is a brief acknowledgment or status ping (for example +1, same, me too, bump, following, thanks, any update(s)?), or is shorter than roughly 15 words and adds no detail. A person qualifies only when at least one of their comments is substantive (not a drive-by). Remove the opener and the PR workers so each person is credited once. May be empty.N, the union of the PR author, the review authors, and the commit authors: gh pr view N --json author,reviews,commits --jq '[.author.login] + [.reviews[]?.author.login] + [.commits[].authors[].login] | unique'. Drop bot accounts (is_bot where available, plus the web-flow, github-actions, and dependabot logins).$issue_list is empty and the issues subsection/section is omitted everywhere.Enumerate the plugins from plugins in Project Context (one parent, plus each child). For every plugin, determine baseline, whether it changed in {range}, and its target. Classify changes against docs/semantic-versioning.md. The governing rules:
plugin.json version is its established baseline. Record the introduction in the changelog, but do not increment. This is the general rule for every future han-* extension, not a one-time exception for the current children.For each plugin, read current from {source}/.claude-plugin/plugin.json and compute baseline:
git cat-file -e {prev}:{source}/.claude-plugin/plugin.json fails, or this is the first release): new plugin. baseline = current, target = current, no bump, mark it new. Skip the rest of the classification for this plugin.baseline = git show {prev}:{source}/.claude-plugin/plugin.json | jq -r .version.baseline = prev# (the parent's version is what the tag tracks, regardless of any directory move). On the first release baseline is empty and the parent is treated like a new plugin set to its current value.Determine whether the plugin changed in {range}:
git diff --name-only {prev}..HEAD -- {source}/ is non-empty.{parent source}/.For a changed child, classify the highest-priority change inside {source}/:
{source}/skills/ was removed or renamed (renaming breaks /skill-name), an agent under {source}/agents/ was removed or renamed, or a commit indicates a breaking behavior change (! in the type, BREAKING CHANGE, a review skill that now auto-posts, and so on). Inspect git diff --name-status {range} -- {source}/ for D/R on SKILL.md or agent paths, and scan commit subjects scoped to that plugin.references/ file, or a new optional capability was added inside {source}/, with no major change present. Inspect the same diff for added SKILL.md / agent files.{source}/.For the parent, the bump level is the maximum across the whole release:
{parent source}/ doc and config fixes) → patch.Take the highest of those. Repo-root changes that do not live inside any plugin directory (for example docs/, README.md, CONTRIBUTING.md) are suite-level: they count toward the parent's level (normally patch) and never bump a child.
Compute proposed from each plugin's baseline: major → (x+1).0.0, minor → x.(y+1).0, patch → x.y.(z+1).
For each changed plugin, compare current to baseline:
highest=$(printf '%s\n%s\n' "{baseline}" "{current}" | sort -V | tail -n1)highest == current and current != baseline): the version was already bumped during development. `target = current`. No confirmation for this plugin. Still compute the expected proposed, and if current is a lower level of bump than the changes warrant, add one non-blocking advisory line to the Step 7 summary.target = proposed; this plugin needs confirmation.target = parent target drives the tag v{parent target}.
AskUserQuestion (header: "Release versions"). State prev, and for every plugin a line of the form {name}: {baseline} → {proposed} ({level}; {evidence}), marking new plugins as new at {current} (no bump), ahead plugins as already at {current}, and unchanged children as unchanged at {current}. Name the specific skills/agents and commits that drove each level. Options: accept the proposed plan (recommended, first); adjust the parent level; adjust a child level; enter explicit versions. Apply the operator's answer to the affected plugins. Record the final plan as the version decision in the Step 7 summary.For every plugin whose target differs from its current (the compute-path plugins from Step 3c, and any plugin the operator edited at 3d), set both files so they read target:
{source}/.claude-plugin/plugin.json version to that plugin's target (Edit).marketplace.json entry: set the version of the plugins[] element whose name equals the plugin name (Edit). Select by name, not by index.Skip any plugin whose target == current (ahead-path or new plugins — their files are already correct). When the entire plan is ahead-path/new (no version differs from current), this step is a no-op; note it and continue.
Follow references/changelog-rules.md exactly. From v3.0.0 onward, each release section is a parent ## v{parent target} heading with one ### {plugin} v{version} sub-heading per plugin that changed (the parent always appears; new and changed children appear; unchanged children are omitted), plus the release-level bookkeeping subsections. Every @mention in the changelog (narrative, PR bullets, issue bullets) is a markdown link to the person's GitHub profile: [@{login}](https://github.com/{login}), never flat text.
### subsections of the ## v{parent target} section, before the next ## v heading, in this order: ### Issues closed in this release (only when $issue_list is non-empty), then ### Pull requests in this release (or the commits form from the fallback). Build the issue bullets from $issue_list and the PR bullets from $pr_list (Step 2), and close the final subsection with the Full changelog: line using the blob link from references/release-notes-format.md. Use Edit.general-purpose agent to write the narrative ## v{parent target} section. The skill already holds this context — paste the actual values into the prompt, do not tell the agent to go read them:baseline → target, and for each changed/new child its name, baseline → target, level, and new/changed status.git log {range} --oneline and git diff {range} --stat, plus, per changed plugin, git diff {range} --stat -- {source}/ so the agent can attribute each change to its plugin, plus a suite-level stat git diff {range} --stat -- docs/ README.md CONTRIBUTING.md CHANGELOG.md .claude-plugin/ labeled as the evidence for the ### han parent section (repo-root changes outside any plugin directory).$pr_list (PR numbers, titles, authors).$issue_list (Step 2): each closed issue's number, title, opener, contributors, closing PR(s), and the relevant fix, so the narrative can credit the issue opener where it describes that fix.$release_context from Step 1 (may be empty).## v{X.Y.Z} sections from CHANGELOG.md verbatim, as the register model.Prompt the agent to: produce only the markdown for the ## v{parent target} section — a one-paragraph summary that names the parent's new version and lists each changed/new child with its version, then one ### {plugin} v{version} sub-heading per changed or new plugin (parent first), each describing only that plugin's changes (using #### for topic subsections when needed), then a release-level ### Deferred (YAGNI) subsection only when work was deliberately cut; when a change closes a tracked issue, name the fix and credit the issue opener inline as a profile link [@{login}](https://github.com/{login}); render every @mention as a [@{login}](https://github.com/{login}) profile link, never flat text; match the register of the two pasted sections; obey every hard voice constraint; attribute every change to the plugin whose directory it touched; never invent changes not present in the commits, diff, PR list, or issue list; return only the section markdown with no preamble. If the agent returns anything else, discard it and re-issue with an explicit "return only the section markdown" reminder.
Insert the returned section directly under the # Han Release Notes title, above the previous newest entry (Edit/Write). Then append the generated bookkeeping subsections to it exactly as in the augment case.
Build the GitHub release body per references/release-notes-format.md: the release's summary paragraph first (the one-paragraph overview from the ## v{parent target} narrative, with no heading above it), then ## What's Changed and the PR lines (* {title} by @{login} in {url}, newest merge last), then an ## Issues closed section built from $issue_list (omitted when empty), then every ### {plugin} v{version} sub-heading of the narrative excluding the ## v{parent target} heading itself and the generated PR/commits/issues bookkeeping subsections (the summary paragraph already leads the body, so it is not repeated here), then the **Full changelog:** blob link and the **Full Changelog:** compare link (compare line omitted on a first release). Compute the blob anchor by lowercasing v{parent target} and deleting every character that is not a-z, 0-9, or - (v3.0.0 → v300). Write the assembled body to /tmp/han-release-notes-v{parent target}.md with the Write tool. Do not assemble it with shell echo/printf.
Print to the operator, regardless of mode:
v{parent target}, the parent's baseline → target and how it was decided (ahead-of-tag → used as-is, or computed-and-confirmed at Step 3), and one line per child (bumped baseline → target, unchanged at version, or new at version).CLAUDE.md states a "Current version:" that does not equal the parent target, note it as a follow-up the operator may want to make (do not edit CLAUDE.md; it is out of scope).## v{parent target} section.--latest, or draft (only if draft_release).If `pause_before_publish` is true: use AskUserQuestion (header: "Publish release") — options: publish now (proceed to Step 8), abort (stop, having changed only local files). Do not push or publish until approved.
If `pause_before_publish` is false (default): continue to Step 8 without pausing.
The operator's request to tag and publish authorizes the commit and push required to do it.
CHANGELOG.md, .claude-plugin/marketplace.json, and every {source}/.claude-plugin/plugin.json that Step 4 changed. Commit with chore(release): v{parent target}. If nothing is staged (augment produced no diff and no version changed — unlikely), skip the commit and note it.v{parent target} already exists (git tag -l v{parent target} non-empty, or git ls-remote --tags origin v{parent target} non-empty), do not recreate it; note that the version was already tagged and continue. Otherwise create it annotated at the release commit: git tag -a v{parent target} -m "v{parent target}".git push origin HEAD then git push origin v{parent target}.Per references/release-notes-format.md, using /tmp/han-release-notes-v{parent target}.md:
gh release view v{parent target} fails): gh release create v{parent target} --title "v{parent target}" --notes-file /tmp/han-release-notes-v{parent target}.md plus --latest, plus --draft only when draft_release is true (never --latest together with --draft).gh release edit v{parent target} --notes-file /tmp/han-release-notes-v{parent target}.md. Add --draft=false only when the operator asked to publish an existing draft (and draft_release is not set). Report it as updated, not created.Report concisely: the full version plan (parent and each child, and how the parent version was decided); the tag (created or already existed); the files committed and the commit; the release URL (or draft URL); whether the CHANGELOG section was augmented or generated; and any advisories from Step 7 (under-bump, CLAUDE.md version drift, non-default release branch). If pause_before_publish was true and the operator aborted at Step 7, report exactly what was changed locally and what was not pushed or published.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.