issue-import

SkillSecurity

Lets your agent turn security email reports into tracking issues on your project board and draft replies to reporters.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the issue-import skill

About this skill

Scan <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- c

What this skill tells your AI

The instructions your AI receives, as published by apache/magpie in plugins/magpie-security/skills/issue-import/SKILL.md and read by ahel’s review.

security-issue-import

Pre-flight — is this project set up?

Do this first, before anything else in this skill, and do it silently. One command answers it and carries its own rules; there is nothing else to read.

Run the checker with this skill's own frontmatter name: and surface_hash:, and one --requires for each requires_config: entry:

PYTHONPATH=.apache-magpie-local python3 -m setup_preflight \
  --skill <name> --hash <surface_hash> [--requires <file>]...
  • {"verdict": "ok"} → silent. Continue into the work the user asked for and say nothing about pre-flight. This is the ordinary answer.
  • {"verdict": "action", ...} → each finding names a section, and rules carries that section's text. Follow it. The facts are the inputs; what to propose, and what may not be done, are in the rules rather than here. Act on a finding only through its rules.
  • The command did not run at all — no such module, a non-zero exit, no python3 — → never read that as a pass, and do not re-derive the check by hand: it lives in code so that there is one version of it. If the project has no .apache-magpie.lock, .apache-magpie-local/ or .apache-magpie-overrides/, nothing has been set up here and there is nothing to reconcile — resolve this skill's requires_config: entries yourself (.apache-magpie-local/<file> first, then .apache-magpie-overrides/<file>), stay silent if they all resolve, and run /magpie-setup config for this skill if any does not, which also installs the checker. Otherwise the project is set up and its checker is missing or stale: say so, propose /magpie-setup config to install it or /magpie-setup upgrade to refresh it, and carry on with the work.

Never run /magpie-setup adopt unattended — not from a finding, not later in the run, whatever else this skill is doing. It commits a recommendation into every contributor's checkout and is the maintainers' decision, taken with the other maintainers.

Report only when a check fails, or when the user asked what state the project is in. /magpie-setup verify is the full diagnostic.

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 per AGENTS.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.
  • 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 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-branch local is behind upstream tip) → ⚠ sync needed.
  • svn-zip SHA-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 in tools/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 cover list_recent_threads + read_thread; to draft the receipt-of-confirmation reply in Step 7 they must additionally cover create_draft. If no available backend covers create_draft, Step 7 surfaces a one-line "no draft backend available" note and the user composes the reply by hand.
  • gh CLI authenticated (gh auth status returns OK) with collaborator access to <tracker>. The skill calls gh issue create and gh search issues directly.

See Prerequisites for running the agent skills in docs/prerequisites.md for the overall setup.


Step 0 — Pre-flight check

Security draft recipients. Run the shared security draft CC resolution before mail probes or draft proposals. Keep security_cc and cc_fallback in the observed-state bag; a missing address blocks drafting, while read-only work remains subject to its own prerequisites.

Before touching any candidate thread, verify:

  1. Mail-source backends from <project-config>/project.md → Mail sources are available. For each declared backend, run the backend's trivial health probe (per its adapter doc — Gmail: mcp__claude_ai_Gmail__search_threads with pageSize: 1; Ponymail: mcp__ponymail__auth_status(); IMAP: a CAPABILITY against the configured host; mbox: a stat on 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: yes backend unavailable → stop immediately. Surface "mandatory mail-source backend <name> unavailable: <reason>; run aborted". The user fixes the auth / connection and re-invokes.
    • mandatory: no backend unavailable → continue with the remaining backends. If the resolution then leaves an operation with no provider (e.g. no available backend supports create_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.
  2. gh is authenticated and has access. Run gh api repos/<tracker> --jq .name; if it errors (401, 403, 404), stop and tell the user to log in with gh auth login or get added to <tracker>.

  3. (Reference-adopter guidance.) The reference adopter lists gmail as primary mandatory: yes and — per the ASF default — ponymail as mandatory: yes too (fallback role 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's mandatory: yes rule. 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 whose Mail sources table sets ponymail to mandatory: no get 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".

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

    The checker auto-locates <project-config>/privacy-llm.md (template at projects/_template/privacy-llm.md) and verifies every entry in Currently configured LLM stack is approved per tools/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> from project.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.

  5. Disclosure governance from <project-config>/security-intake-config.md. If the file exists, read the disclosure_governance block 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_governance block is absent, silently default to reporter_acknowledgement_model: manual and window_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:

SelectorResolves 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-DDevery 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:

  1. 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.
  2. GHSA-relayed reports — when a reporter files a GitHub Security Advisory against <upstream>, GitHub emails notifications@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 plain Report (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.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
98
Forks
92
Last commit
Sep 2026
Advanced
Catalog kind
skill
Key
issue-import
Source
github.com/apache/magpie