magpie-security-issue-import
SkillSecurityScan <security-list> for reports that have not yet been copied into <tracker> as tracking issues, present the proposed imports to the user, and — defaulting to *import unless the user rejects upfront* — create the tracking issues with the `Needs triage` project-board status and draft a receipt-of- confirmation reply to each reporter. This is the first step of the handling process: the entry point that converts an inbound email thread into a tracker the rest of the skills (security-issue-sync, security-issue-fix, generate-cve-json) operate on.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the magpie-security-issue-import skill
What this skill tells your AI
The instructions your AI receives, as published by apache/magpie in skills/security-issue-import/SKILL.md and read by ahel’s review.
security-issue-import
This skill is the on-ramp of the security-issue handling process.
It converts an inbound <security-list> email thread into
an <tracker> tracking issue that follows the repo's issue
template, then drafts the receipt-of-confirmation reply to the reporter.
It never sends email. It never creates a tracker for a candidate the
user has explicitly rejected. It never assumes a report is valid —
the validity / invalid / CVE-worthy decision still happens later in
the discussion on the created tracker (Step 3 of
README.md).
Golden rule — propose, then default to import. Every import this
skill performs is a proposal that lists the candidate emails, the
extracted fields, and the draft confirmation reply. The user's
default disposition for any Report or forwarder-relayed
candidate (the latter classified by the optional
security-issue-import-via-forwarder
sub-skill when forwarders.enabled is non-empty) is
"import as a new tracker landing in Needs triage";
the user only has to type back when they want to deviate from that
default — skip NN to reject a candidate upfront with no reply, or
NN:reject-with-canned <name> to reject upfront and draft a
specific canned negative-assessment / out-of-scope reply. A bare
all (or no reply at all to the proposal — the user typing
"go", "proceed", "yes, all") means "import every
non-rejected candidate as proposed". The skill must still surface
each candidate one-by-one in the proposal so the user can scan and
override if needed; what the skill must not do is sit on a report
waiting for an explicit per-candidate green light. The bias is
toward landing trackers — a wrongly-imported report is cheap to
close at Step 5 / 6 of the handling process; a wrongly-skipped one
gets buried in the inbox and the reporter is left without a
disposition.
Golden rule — rejection means no tracker, ever. When the user
rejects a candidate upfront — any of skip NN,
NN:reject-with-canned <name>, an explicit "reject 1",
"mark 1 invalid", "don't import 1", or a cancel / none /
"hold off" on the whole proposal — the skill must not create
a tracker for that candidate. This holds even when the user also
asks for a canned reply to be drafted: the draft is a courtesy to
the reporter, the absence of a tracker is the disposition. There is
no "create the tracker so the team can close it as invalid later"
path; if the team has decided pre-triage that the report is
invalid, the audit trail lives on the Gmail thread and on the
canned-responses.md precedent, not in a tracker that exists only
to be closed. A tracker is created only when the candidate is
imported as a real Report (or a forwarder-relayed candidate
classified by the
security-issue-import-via-forwarder
sub-skill) for triage.
Non-import candidate classes (automated-scanner,
consolidated-multi-issue, media-request, spam,
cross-thread-followup, cve-tool-bookkeeping) keep the original
"propose first, apply only on explicit confirm" rule — those never
default to a tracker.
Golden rule — confidentiality. The inbound thread on
<security-list> is private. The skill may paste the
email body verbatim into the created <tracker> tracking
issue (that repo is also private). It must never paste the
report content into a public surface — not into <upstream>, not
into a public GHSA, not into any comment on a public repo. The same
confidentiality rule documented in the "Confidentiality of
<tracker>" section of AGENTS.md
applies in full.
Golden rule — every <tracker> / <upstream> reference is
clickable in the surface it lands on. Whenever this skill emits
a reference to a tracker issue, PR, or comment — the proposal
shown to the user before import, the created tracker issue body
(observed-state dump, sibling-tracker cross-links, prior-rejection
cross-links, fix-already-public PR pointers), the receipt-of-
confirmation draft email reply, the recap output — the reference
must be one click away in whatever surface it lands on:
-
On markdown surfaces (the created tracker issue body, the draft email reply destined for the
<security-list>thread, any markdown-rendered cross-link list): use the markdown link form perAGENTS.md§ Linking tracker issues and PRs:- Sibling
<tracker>issue:[<tracker>#NNN](https://github.com/<tracker>/issues/NNN) - Public
<upstream>PR (e.g. fix-already-public match):[<upstream>#NNN](https://github.com/<upstream>/pull/NNN) - Comment: link to the
#issuecomment-<C>anchor.
- Sibling
-
On terminal surfaces (the proposal shown to the user before import, the recap output): wrap the visible short form (
<tracker>#NNN,<upstream>#NNN) in OSC 8 hyperlink escape sequences (\e]8;;<URL>\e\\<short>\e]8;;\e\\) so modern terminals (iTerm2, Kitty, GNOME Terminal, WezTerm, Windows Terminal, …) render the short text as clickable. Where OSC 8 is unsupported (CI logs, dumb terminals), fall back to printing the bare URL on the same line after the number.
Bare #NNN with no link wrapper of any kind is never acceptable.
The created tracker issue is read by the security team who drill
into the cross-links to assess; the draft email reply lands on
<security-list> where the reporter needs the references to be
one click away. Both surfaces are private, but <tracker> URLs
themselves are public-safe per the
Confidentiality of <tracker>
rule — what stays private is the contents the link points at.
Self-check before posting any draft email or creating any
tracker issue: grep the body for bare #\d+ / <tracker>#\d+
tokens that aren't already inside a markdown link or an OSC 8
wrapper, and convert any match.
Adopter overrides
Before running the default behaviour documented
below, this skill consults
.apache-magpie-local/security-issue-import.md (personal, gitignored) and .apache-magpie-overrides/security-issue-import.md (committed, project-wide)
in the adopter repo if it exists, and applies any
agent-readable overrides it finds. See
docs/setup/agentic-overrides.md
for the contract — what overrides may contain, hard
rules, the reconciliation flow on framework upgrade,
upstreaming guidance.
Hard rule: agents NEVER modify the snapshot under
<adopter-repo>/.apache-magpie/. Local modifications
go in the override file. Framework changes go via PR
to apache/magpie.
Snapshot drift
Also at the top of every run, this skill compares the
gitignored .apache-magpie.local.lock (per-machine
fetch) against the committed .apache-magpie.lock
(the project pin). On mismatch the skill surfaces the
gap and proposes
/magpie-setup upgrade.
The proposal is non-blocking — the user may defer if
they want to run with the local snapshot for now. See
docs/setup/install-recipes.md § Subsequent runs and drift detection
for the full flow.
Drift severity:
- method or URL differ → ✗ full re-install needed.
- ref differs (project bumped tag, or
git-branchlocal is behind upstream tip) → ⚠ sync needed. svn-zipSHA-512 mismatches the committed anchor → ✗ security-flagged; investigate before upgrading.
Prerequisites
Before running, the skill needs:
- At least one configured mail-source backend per
<project-config>/project.md → Mail sources. The skill treats every backend the same way — through the abstract operations defined intools/mail-source/contract.md(list_recent_threads,read_thread,list_drafts,list_sent_since,create_draft,thread_url). Reference adapters:gmail(full read+write),ponymail(read-only ASF archive),imap(stub),mbox(read-only offline archive — stub). To discover new reports the configured backends must collectively coverlist_recent_threads+read_thread; to draft the receipt-of-confirmation reply in Step 7 they must additionally covercreate_draft. If no available backend coverscreate_draft, Step 7 surfaces a one-line "no draft backend available" note and the user composes the reply by hand. ghCLI authenticated (gh auth statusreturns OK) with collaborator access to<tracker>. The skill callsgh issue createandgh search issuesdirectly.
See
Prerequisites for running the agent skills
in docs/prerequisites.md for the overall setup.
Step 0 — Pre-flight check
Before touching any candidate thread, verify:
-
Mail-source backends from
<project-config>/project.md → Mail sourcesare available. For each declared backend, run the backend's trivial health probe (per its adapter doc — Gmail:mcp__claude_ai_Gmail__search_threadswithpageSize: 1; Ponymail:mcp__ponymail__auth_status(); IMAP: aCAPABILITYagainst the configured host; mbox: astaton the archive path) and record the result in the skill's observed-state bag. Apply the contract's resolution rule to figure out which backend serves which op for this run.mandatory: yesbackend unavailable → stop immediately. Surface "mandatory mail-source backend<name>unavailable:<reason>; run aborted". The user fixes the auth / connection and re-invokes.mandatory: nobackend unavailable → continue with the remaining backends. If the resolution then leaves an operation with no provider (e.g. no available backend supportscreate_draft), the skill records "no<op>backend available" in the observed-state bag and the relevant downstream step omits that proposal with a clear hand-back to the user.- Every declared backend healthy → proceed; the observed-state bag records one provider per op so every dispatch later is unambiguous.
-
ghis authenticated and has access. Rungh api repos/<tracker> --jq .name; if it errors (401, 403, 404), stop and tell the user to log in withgh auth loginor get added to<tracker>. -
(Reference-adopter guidance.) The reference adopter lists
gmailas primarymandatory: yesand — per the ASF default —ponymailasmandatory: yestoo (fallbackrole for drafts, since PonyMail is read-only). So for the reference flow both backends are pre-flight prerequisites: a Gmail-MCP failure stops the run (drafts have no home), and a PonyMail-MCP miss — not registered, or registered but unauthenticated for the private<security-list>archive — stops it too, per item 1'smandatory: yesrule. Gmail handles reads of just-arrived inbound mail and all draft creation; PonyMail handles archive lookups (and is the primary read path when authenticated). Adopters whoseMail sourcestable setsponymailtomandatory: noget the old degrade-quietly behaviour; the step-by-step references to "Gmail" below should be read as "the backend the resolution rule picked for the relevant op". -
Privacy-LLM contract. This skill reads
<security-list>bodies that may contain third-party PII the reporter discloses about other people. Run the gate-check first — non-zero exit is a hard stop:uv run --project <framework>/tools/privacy-llm/checker \ privacy-llm-checkThe checker auto-locates
<project-config>/privacy-llm.md(template atprojects/_template/privacy-llm.md) and verifies every entry in Currently configured LLM stack is approved pertools/privacy-llm/models.md. In addition, verify:~/.config/apache-magpie/is writable (the redactor's mapping file lives there);- the configured collaborator source is reachable via
gh api(default:<tracker>fromproject.md); - the redaction-tuning knobs (collaborator exemption, enabled field types) are loaded into the skill's observed-state bag — they apply at filter-time below.
Each subsequent body fetch in Steps 4 / 7 / 7g (template- field extraction, draft assembly, recap) follows the redact-after-fetch protocol in
tools/privacy-llm/wiring.md; the receipt-of-confirmation draft assembly follows the reveal-before-send protocol when (and only when) the draft references a third-party identifier. -
Disclosure governance from
<project-config>/security-intake-config.md. If the file exists, read thedisclosure_governanceblock and load these two keys into the observed-state bag for use in Step 7:reporter_acknowledgement_model—manual|auto|none. Controls whether and how the receipt-of-confirmation reply is drafted (Step 7.4).window_days— integer; the CVD window in calendar days, used as the disclosure deadline hint when composing the acknowledgement draft.
If the file does not exist or the
disclosure_governanceblock is absent, silently default toreporter_acknowledgement_model: manualandwindow_days: 90. A missing file is not a stop condition — adopters who have not yet created this config receive the same ASF defaults the skill has always applied.
If a mandatory: yes mail-source backend or the gh check fails,
do not proceed — the skill would fail mid-flow otherwise,
leaving half-built state (a draft on the wrong thread, or a tracker
with no receipt reply). Fail fast instead. mandatory: no backends
degrade quietly per the contract's resolution rule. A privacy-llm
pre-flight failure is also a hard stop — the redactor's mapping
store and the collaborator-source lookup are both load-bearing for
every subsequent body read.
Inputs
Before running, resolve the user's selector into a concrete set of candidate Gmail threads:
| Selector | Resolves to |
|---|---|
import new (default) | every security@ thread received in the last 14 days that has not yet been imported as an issue and has not already been answered-and-closed on-thread |
import since:YYYY-MM-DD | every security@ thread received since the given date that is not yet imported |
import thread:<id> | the single Gmail thread with that threadId — useful for re-importing after a manual discard, or for picking up a single message the automatic scan missed |
import last 30d / import all / import last Nd (explicit request only) | a wider sweep — use when the skill has not been run in a while or the user is doing a backlog catch-up. The all alias spans disclosure_governance.window_days days (default 90) from <project-config>/security-intake-config.md. |
If the user supplies no selector, default to import new (14-day window).
Why the default is 14 days. Most reports that land on security@
fall into one of three steady-state buckets: (a) imported as a tracker
within days of arrival, (b) answered on-thread with a canned negative
response that the reporter accepts silently, or (c) obvious spam the
triager ignores. None of those need a second look past 14 days. Widening
the default window past two weeks would keep re-surfacing the same
already-handled threads every sync run, which is noise. The user can
always pass import last 30d or import all explicitly when a deeper
sweep is genuinely warranted (e.g. after a long quiet period, or during
a backlog audit).
Step 1 — List candidate threads from Gmail
Search <security-list> for inbound reports, excluding the
tooling / GitHub-notification / mailing-list chatter that isn't a
report:
Use the canonical candidate-listing query template from
tools/gmail/search-queries.md;
substitute the adopting project's <security-list-domain> and the
project's GitHub-notification exclusions — both declared in
<project-config>/project.md.
Backend selection. Candidate listing is one of the cases where Gmail remains primary even when PonyMail MCP is enabled: the inbox is where just-arrived inbound reports land with the lowest latency, and the import skill's sole purpose is converting those freshly-arrived threads into trackers. The PonyMail archive lags the inbox by minutes-to-hours for brand-new messages, which is exactly the window this skill most cares about.
When PonyMail MCP is enabled and authenticated (Step 0) and
<security-list> is in .apache-magpie-overrides/user.md →
tools.ponymail.private_lists, run the archive as a paired
authoritative check against the Gmail result set:
mcp__ponymail__search_list(
list: "security",
domain: "<project>.apache.org",
timespan: "lte=30d",
emails_only: true
)
Cross-reference the returned summaries against the Gmail result
set by Message-ID. Surface two classes of mismatch as extra
candidates in Step 5:
- In PonyMail, not in Gmail → note "seen in the archive, not in this user's Gmail — LDAP-only subscription, Gmail-filter miss, or wrong account". Often worth importing; always worth surfacing.
- In Gmail, not in PonyMail → note "in Gmail inbox, not yet in the archive — archive-indexing lag; Gmail snapshot is the authoritative source for now". Proceed with Gmail-only data for this thread; a future sync run will reconcile once the archive catches up.
When PonyMail MCP is disabled, unauthenticated, or the private list is not in the user's allowlist, skip the paired-check query and proceed Gmail-only.
Do not exclude -from:<security-list>. That address is used
for three very different message types — CVE-tool bookkeeping,
ASF Security Team forwarding of inbound reports, and ad-hoc ASF
Security discussion / advice. Blanket-excluding the sender would drop
the forwarded reports along with the bookkeeping noise, so the
bookkeeping emails are filtered out at Step 3 by subject pattern
instead — see the cve-tool-bookkeeping row of the classification
table.
Do not exclude -from:notifications@github.com wholesale. GitHub
uses this address for two distinct categories of messages:
- Tracker-mirror notifications — when an action lands on a
tracker issue (comment, label, close), GitHub emails every
subscriber. These arrive with subject
[<tracker-repo>] ...and are not import candidates — they describe an existing tracker. - GHSA-relayed reports — when a reporter files a GitHub
Security Advisory against
<upstream>, GitHub emailsnotifications@github.com → <security-list>with subject[<upstream>] ... (GHSA-...). These are import candidates. A GHSA relay is not a distinct class — at Step 3 classify it as a plainReport(the GHSA ID is captured as a de-dup signal and as provenance, not as the classification) and proceed to field extraction.
Filter the mirror notifications at Step 1 only by the project's
declared dedicated noreply mirror addresses (e.g.
<tracker-repo>@noreply.github.com, declared in
<project-config>/project.md).
Do not blanket-exclude notifications@github.com — the
remaining tracker-mirror chatter on notifications@github.com is
caught at Step 2 (threadId dedup against existing tracker bodies)
and Step 2-bis (already-answered detection).
The canonical query template in
tools/gmail/search-queries.md
omits the blanket exclusion; project-specific <project-config>/project.md
declarations enumerate dedicated mirror noreply senders only.
Mandatory second pass — run a positive GHSA query. The rule above is a negative one, and negative rules are not self-enforcing: an exclusion added anywhere for noise reduction removes the whole GHSA intake channel, and the miss is invisible — nothing reports that a report was filtered out. So every import scan also runs the GHSA-advisory query, which checks the channel by construction and cannot be filtered away by an exclusion elsewhere. Union its hits with the candidate-listing query's before Step 2.
For each hit whose subject carries [<upstream>] ... (GHSA-...):
- Treat it as a
Reportcandidate, not tracker-mirror noise. - The advisory body is confidential — it lands in the private tracker only, per the confidentiality golden rule; never echo it to a public surface.
- Prefer the advisory record API
(
gh api repos/<upstream>/security-advisories/<GHSA>). If it 404s, the operator is not yet a collaborator on that specific advisory — this is an access state, not a missing advisory. Extract the report from the notification email body and flag the admin hand-off: someone with advisory-admin rights must add the operator as a collaborator before the record API and the reporter-reply path become usable. See the GHSA contract insecurity-issue-sync'sgithub-advisory.md.
Adjust the time window per the user's selector (since: → newer_than:
or after:; import all → newer_than:90d).
Run the query via mcp__claude_ai_Gmail__search_threads (see
tools/gmail/operations.md).
For each result, record threadId — the downstream de-duplication
hinges on this.
Do not read the thread bodies yet. Body reads cost Gmail budget and most threads will be filtered out at Step 2.
Step 2 — Deduplicate against existing issues
For each candidate threadId, check whether that ID already appears in
an <tracker> issue body. The sync skill records each thread
ID in the "Security mailing list thread" field of the tracking issue
(either as the <mail-archive-url>/thread/<id> URL or as a textual note
containing the Gmail threadId). One gh search issues call is
enough:
gh search issues "<threadId>" --repo <tracker> --match body --limit 5 \
--json number,title,state,url
If the search returns any hit, the thread is already imported — skip
it. Do not propose re-importing (that would create a duplicate
tracker). If the user explicitly passed import thread:<id> and the
thread is already imported, tell the user and link the existing issue
rather than trying to create a duplicate.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 91
- Forks
- 91
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
magpie-security-issue-import- Source
- github.com/apache/magpie