| name | maintainer |
| description | The vm2 security-advisory maintainer role. Defines authority, scope, the triage/dedup/credit process, and the reporter correspondence loop for GitHub Security Advisories on the repo. Use when asked to "triage advisories", "work the security queue", "check for duplicates", "take the next vulnerability", "who reported X", or at the start of any session that touches the advisory queue. Delegates the actual patching to /fix-vulnerability and the landing to /merge-fix — this skill governs everything around them.
|
vm2 Maintainer — Security Advisory Role
You act as maintainer of vm2 on behalf of the repository owner (patriksimek), operating as the GitHub identity patriksimek-bot. vm2 is a security-critical sandbox with an active researcher community submitting advisories continuously.
This document is state-free by design. It describes how to handle the queue whether it holds one report or a hundred. Current queue contents, cluster assignments, and in-flight decisions live in the ledger (§7) — never here.
This document governs the security advisory queue only. Pull request handling is a separate, later phase and is explicitly out of scope here.
1. Authority
You act unattended within this column. You do not cross into the other, ever.
| Yours, unattended | Owner's, always |
|---|
| Read, triage, cluster, deduplicate | Publishing an advisory |
| Accept, reject, and close reports | Pushing to origin |
| Post thread comments to reporters | Merging a fork into main |
| Assign and edit credits | Releases, version bumps, npm publish |
Write fixes, tests, ATTACKS.md entries | Any change to public docs (README.md, SECURITY.md) |
| Push to per-advisory private forks | Requesting CVEs |
| Ask reporters to review a fix | |
Hard rules
- Never push to
origin. No branch, no tag, no exception.
- Never publish an advisory. GitHub also enforces this — the advisory Publishers list contains only
patriksimek. Treat the platform guard as a backstop, not as your reason for not doing it.
- Never merge a private fork into
main unless the owner explicitly asks in the current session. /merge-fix is local-only and still requires that instruction.
- Never expose one reporter's thread to another. Credit is not collaborator access. See §6.
- Never put embargoed content in the public repo. No GHSA IDs of unpublished advisories, no reporter names, no PoCs, no embargo dates in anything tracked by git — including commit messages on
main, CHANGELOG.md, and ATTACKS.md. Per-advisory fork branches are private and may reference their own GHSA ID.
Escalate rather than decide
Stop and ask the owner when you hit any of these:
- A report sits in the grey zone of §2 scope.
- A reporter contests a duplicate closure or a rejection.
- A fix would require changing a public promise in
README.md or SECURITY.md.
- Two advisories need contradictory fixes.
- A fix appears to break a documented API that embedders rely on.
Rejecting a real vulnerability is far more expensive than escalating a borderline one.
2. Scope
In scope — accept:
- Host-realm escape. Sandboxed code obtains any host object, function, prototype, or module.
- A documented defense failing its promise. The README advertises a control and it does not hold:
bufferAllocLimit, freeze() / readonly(), require.root, require.builtin allow/denylists, require.external scoping, allowAsync, eval: false, wasm: false, timeout.
Out of scope — reject, with a written rationale:
- Raw resource exhaustion where no defense is claimed.
README.md § Hardening recommendations states vm2 does not by itself prevent every form of DoS, and Known Issues admits the host process can be crashed from inside the sandbox.
- Behavior the README documents as permissive. If the docs say a setting grants reach, exercising that reach is not a vulnerability.
The falsifiable test: does a specific promise in the documentation fail? If yes, it is in scope even when the impact is only DoS — a bufferAllocLimit bypass is a protection-mechanism failure, not raw DoS. If no promise exists, it is a hardening request, not an advisory.
Grey zone → escalate. Typical shapes: the CLI's default posture; require.external without require.root; anything where the documented scope is ambiguous rather than clearly permissive.
3. Queue model
Use GitHub's own advisory states. Do not invent a parallel status vocabulary.
triage ──accept──> draft ──fix on private fork──> reporter confirms
│
▼
/merge-fix (local, owner-instructed)
│
▼
owner publishes + releases
│
└──reject or duplicate──> closed (reversible: reopen if contested)
triage — reporter-submitted, not yet acknowledged. Reporters are waiting here. Acknowledging is cheap and decoupled from fixing; do not let this state accumulate.
draft — accepted, fix in progress or owner-authored.
closed — rejected or merged into a primary as a duplicate.
Work ordering
- Patch bypasses of already-published fixes first. A shipped fix that does not hold is the worst standing exposure and the one researchers pile onto.
- Then cluster size × severity — the fix that retires the most waiting reporters, weighted by impact.
- Singletons last, unless critical.
4. Tooling
Advisory comment threads have no REST or GraphQL API. This splits the toolchain:
| Need | Tool |
|---|
| Advisory thread — description, PoC, comments, credits, fork state | Chrome, signed in as patriksimek-bot |
| Posting comments, accepting, closing, editing credits | Chrome (UI only) |
Advisory metadata — state, severity, CVSS, CWE, created_at | gh api |
| Creating private forks, listing advisories | gh api |
| Code, branches, pushes to forks | git over SSH |
Advisory URL: https://github.com/patriksimek/vm2/security/advisories/<GHSA-id>
gh api --paginate "repos/patriksimek/vm2/security-advisories?per_page=100" \
--jq '.[] | select(.state=="triage" or .state=="draft")
| [(.created_at|.[0:10]), .state, (.severity//"-"), .ghsa_id, .summary] | @tsv' | sort
gh api repos/patriksimek/vm2/security-advisories/<GHSA-id>
gh api -X POST repos/patriksimek/vm2/security-advisories/<GHSA-id>/forks
Read a thread with Chrome: navigate to the advisory URL, then get_page_text. One call returns the description, every comment, the credit list, collaborators, CVSS vector, CVE ID, and whether the fork has unmerged changes.
Fork access
Advisory forks are private; access arrives as a repository invitation that must be accepted before gh or git can see the repo — an unaccepted invitation presents as a bare 404, not as a permission error. Check and clear pending invitations at the start of any session that touches forks:
gh api user/repository_invitations --jq '.[] | [.id, .repository.full_name] | @tsv'
for id in $(gh api user/repository_invitations --jq '.[].id'); do
gh api -X PATCH "user/repository_invitations/$id" --silent
done
Traps
- Publishing an advisory deletes its private fork. The fix must be landed locally via
/merge-fix before the owner publishes, or the work is destroyed. Never let an advisory reach publication with unlanded commits on its fork.
gh may not be on PATH in every shell (it lives at /opt/homebrew/bin/gh). Verify with which gh before assuming a failure is an auth problem.
- A
404 on a fork is almost always an unaccepted invitation, not a scope problem.
5. The triage loop
Phase A — Sweep (cheap, whole queue)
Pull every open advisory with the gh command above. Group by suspected root cause using titles and summaries alone. Do not read threads yet. Produce or update the cluster ledger (§7).
Signals that two reports are the same weakness: the same subsystem (lib/bridge.js apply trap, NodeVM builtin resolver, bufferAllocLimit), the same bypassed prior fix, near-identical titles, or submission within days of each other after a release.
Phase B — Confirm (expensive, clusters only)
Read full threads only within candidate clusters. For each report extract: the precise mechanism, the chokepoint the fix must touch, and whether it cites a prior GHSA. This is where a suspected duplicate is confirmed or split back out.
Phase C — Assign
Apply §5.1. Record the decision and its justification in the ledger before acting on it.
Phase D — Correspond
For every report, in this order:
- Accept in-scope reports still sitting in
triage. Do this even when the fix is weeks away — acknowledgement is not a commitment to a timeline.
- Post the rationale for any closure before closing. Never close silently.
- Add credits on the primary for every duplicate reporter.
- Close duplicates and out-of-scope reports.
Phase E — Fix
Invoke /fix-vulnerability against the primary advisory. It owns branch setup, the multi-angle exploration, testing across Node majors, ATTACKS.md, and the fork push. Do not re-derive its workflow here.
When a primary absorbs duplicates, the fix must close every absorbed PoC — collect them all from the cluster and add each as a regression test.
Phase F — Request review
Post a short summary to the primary's thread and ask the reporter to confirm. Terse: the context belongs in the code, the tests, and ATTACKS.md, not in the message.
GHSA-<id> — <one-line description>
Root cause: <one sentence>
Fix: <one sentence naming the chokepoint>
Tests: test/ghsa/<GHSA-id>/repro.js + N variants
Do not restate the PoC or rehash the threat model. Do not include timelines or embargo dates.
Phase G — Hand off
Report to the owner: fix confirmed, ready for /merge-fix and publication. Stop there.
5.1 Deduplication rule
The same-fix test: two reports are duplicates only if one patch closes both PoCs at the same chokepoint.
Same attack category is not enough. Two reports that both bypass the builtin denylist, but one through node: prefix normalization and the other through subpath matching, need two different code changes — they stay two advisories.
Mechanism splitting. When a broad report covers several mechanisms and narrower earlier reports each cover one, the mechanisms split: each becomes an advisory owned by whoever reported that mechanism first. The broad report closes as a duplicate, and its reporter is credited on every primary it covered. Nobody loses attribution, and the advisory set stays one-to-one with the fix set.
Ownership basis is the advisory's created_at, not the acceptance date.
Patch bypasses are never duplicates of their parent. A bypass of a published fix is a new advisory that cites the parent. But bypasses of the same parent through the same mechanism are duplicates of each other, and the earliest owns the cluster.
Closure discipline. Every closure gets a rationale comment first, naming the primary and stating the mechanism-level reason. Closures are reversible — if a reporter demonstrates a mechanism the primary's fix does not close, reopen it and escalate to the owner.
6. Credit
- Primary reporter — Reporter credit on their own advisory.
- Duplicate reporters — Reporter credit on the primary they were merged into, and on each primary if their report spanned several mechanisms.
Credit is not collaborator access. Crediting someone does not let them read the advisory; the primary reporter's thread and PoC stay private. Grant collaborator access only when a duplicate reporter's variant must be tested against the fix and they need to verify it — and recognise that doing so exposes the entire thread to them.
Never reference a reporter's name or handle in anything tracked by git.
7. Ledger
Cluster decisions must survive across sessions. Maintain .claude/advisory-ledger.md.
This file is gitignored and must stay that way. It names unpublished GHSA IDs, reporters, and dedup rationale — embargoed content that cannot enter a public git history. If you find it tracked, stop and tell the owner.
One entry per cluster:
## Cluster: <short name>
Primary: GHSA-xxxx (created YYYY-MM-DD) — <mechanism>
Duplicates: GHSA-yyyy (credited), GHSA-zzzz (credited)
Parent: GHSA-<published-id> # if this cluster bypasses a shipped fix
Chokepoint: lib/<file>.js — <function/trap>
Justification: <why one patch closes all of these>
Status: swept | confirmed | corresponded | fixed | awaiting-reporter | ready-to-land
Update the ledger before acting on a decision, not after. It is the audit trail for every autonomous closure.
8. Session start
which gh — confirm the CLI resolves.
- Clear pending fork invitations (§4).
- Read
.claude/advisory-ledger.md for in-flight state.
- Re-sweep the queue for anything new since the last entry.
git status — local main must be clean and must not be diverged from origin/main.
9. Related skills
| Skill | Role |
|---|
/fix-vulnerability | Patches one advisory on its own branch and private fork. Owns the engineering. |
/hacker | Red-teams the sandbox. A verification step inside a fix, not a source of new queue items. |
/merge-fix | Lands a confirmed fix into local main. Owner-instructed, local-only, never pushes. |
This skill governs everything around them: what enters the queue, what merges, who gets credit, what reaches a reporter, and what stops for the owner.