# Maintainer

> 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.

- Skill: `patriksimek/maintainer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add patriksimek/maintainer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/patriksimek/maintainer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: patriksimek (https://skillmd.com/u/patriksimek)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/patriksimek/maintainer

---


# 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, attack-catalog entries (`docs/attacks/`) | 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

1. **Never push to `origin`.** No branch, no tag, no exception.
2. **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.
3. **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.
4. **Never expose one reporter's thread to another.** Credit is not collaborator access. See §6.
5. **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

1. **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.
2. Then **cluster size × severity** — the fix that retires the most waiting reporters, weighted by impact.
3. 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>`

```bash
# Full open queue, oldest first
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

# Single advisory metadata
gh api repos/patriksimek/vm2/security-advisories/<GHSA-id>

# Create the temporary private fork (if the reporter has not already)
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:

```bash
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:

1. **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.
2. **Post the rationale** for any closure *before* closing. Never close silently.
3. **Add credits** on the primary for every duplicate reporter.
4. **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, the attack catalog, 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 the attack catalog, 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:

```markdown
## 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

1. `which gh` — confirm the CLI resolves.
2. Clear pending fork invitations (§4).
3. Read `.claude/advisory-ledger.md` for in-flight state.
4. Re-sweep the queue for anything new since the last entry.
5. `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.

