Skip to content

The github-issues quoting hardening needs a carrier an agent actually loads #152

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

#151 added a quoting overlay for the synced github-issues skill as a section in plugins/github/README.md, on the reasoning that a file outside skills/ survives an update-agent-skills pull.

It survives, but nothing reads it. This repository's own AGENTS.md states that a plugin's resources are skills/, agents/ and a bundled .mcp.json, each auto-discovered from its directory. A plugin's root README is not a resource. So an agent invoking github-issues receives skills/github-issues/SKILL.md and its references/, whose gh api examples still build -f values from issue text — the exact pattern the overlay warns about.

Raised as a P1 by Codex review on #151 and confirmed against AGENTS.md.

Why it matters

This is worse than an unfixed trap, because the repository now carries a test asserting the overlay exists. A future reader sees a guard passing and concludes the quoting risk is handled, when no agent has ever seen the guidance.

The constraint that makes it awkward

The obvious carrier — the skill itself — is a synced copy, reverted by the daily update-agent-skills workflow. Editing it there is silently undone, which is what pushed #151 outside skills/ in the first place.

Options, none free

  1. A plugin-authored skill carrying the safe-quoting guidance. Auto-discovered, and not overwritten because the sync only re-pulls the skills it owns. But every skill in this repository is currently synced from an upstream, so this introduces the first locally-authored skill into a marketplace every agent install consumes — a blast-radius decision, and it adds a permanently-listed skill for one caveat.
  2. A post-sync overlay step in update-agent-skills that re-applies a patch after each pull. Keeps the guidance in the file agents actually load, at the cost of a patch that must survive upstream edits.
  3. Upstream it to the skill's owner. The durable fix, and blocked for unattended runs by the ask-before-upstream-creates rule.
  4. Accept and document that the trap is upstream's, and drop the local layer rather than keep a hardening nothing loads.

Also to fold in

Codex's second P1 on the same PR: the --input example starts from populated $title / $description and never shows how untrusted text reaches them, which is the step where the injection actually occurs. Whatever carrier is chosen must show the ingress — stdin, argv, or an execution API that does not re-parse — rather than assuming the variables exist. Copied literally the current snippet also sends empty values, or fails under set -u.

Acceptance criteria

  • A decision is recorded on which carrier the guidance lives in, with the blast-radius trade-off stated.
  • The guidance is reachable by an agent invoking github-issues through a normal install, demonstrated rather than asserted.
  • The guidance shows a safe ingress for untrusted text, not just a safe command shape.
  • Any guard asserts the guidance is in the load path, not merely that a file exists.

Blocks #151, which is parked as a draft. Part of #116.

Activity

  1. devantler commented on Aug 21, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Decision: the carrier is upstream, and the local overlay is dropped

    Recording the call this issue asks for, so #151 stops sitting parked as a draft.

    What settled it

    Two standing maintainer directions, both on the same question — a defect in a synced third-party
    skill
    , where the local copy is reverted by update-agent-skills:

    github-issues declares metadata.github-repo: https://github.com/github/awesome-copilot, so it is
    the same situation as #109 rather than a new one.

    Why options 1, 2 and 4 are all closed by that

    Options 1 (a locally-authored skill) and 2 (a post-sync patch step) are exactly the "custom
    workaround" the maintainer ruled out. Option 4 (accept and document) is what the overlay already
    effectively is, and it is the shape #151 currently ships.

    Confirming there is no fifth carrier — this was checked, not assumed

    A plugin's auto-discovered resources are exactly skills/, agents/ and a bundled .mcp.json
    (this repository's AGENTS.md; plugin.json carries no component-path fields). The root README.md
    is the human-facing index, and resources/ is explicitly "ancillary, explicitly linked
    human-consumed assets"
    and deliberately not counted as a plugin resource.

    So there is no first-class mechanism to attach loadable guidance to a synced third-party skill. The
    review finding on #151 is correct: an agent invoking github-issues receives SKILL.md and its
    references/, and never the README section.

    The blast-radius trade-off, stated

    Option 1 was the only rejected option with a real upside — it would be in the load path. It is
    rejected because it introduces the first locally-authored skill into a marketplace every install
    consumes, permanently listing a skill for one caveat, and because it still leaves the upstream trap
    in place for every other consumer of awesome-copilot. Upstreaming fixes it once, for everyone,
    including us after the next sync.

    Consequence for #151

    Closing it. It is a custom workaround that additionally does not work, and its guard asserts the
    overlay exists rather than that it is loadable — so merging it would leave a passing test implying
    the quoting risk is handled when no agent has ever seen the guidance. That is the "worse than an
    unfixed trap" case this issue opens with.

    What is actually missing here

    Not analysis. The patch below is ready to submit.

    Blocker: missing authority (upstream contribution) | last-verified 2026-08-21: unchanged

    github/awesome-copilot is outside devantler-tech. Opening anything there needs confirmation that
    the repository is unrelated to professional work, a separate explicit approval to create an upstream
    artifact, and a check of that project's policy on AI-assisted contributions — none of which an
    unattended run can obtain. Same boundary as #109 and .github#85.

    The prepared upstream patch

    Target: skills/github-issues/SKILL.md in github/awesome-copilot.

    The skill writes every issue field as gh api … -f title="…" -f body="…". That shape is safe for the
    literal examples shown, and unsafe at the step the skill never covers: how untrusted text (an error
    log, a user report, a page the agent read) reaches those variables. Add a short section covering the
    ingress, not just the command shape:

    • Pass untrusted text through stdin or a file, never by interpolating it into the command line:
      gh api … -X POST -f title="$title" --field-from-file body=/path/to/body.md, or build the payload
      as JSON with jq -n --arg / --rawfile and pipe it to gh api --input -.
    • --input - reads a JSON body from stdin, so no shell quoting layer ever sees the content.
    • Show the variables being populated from the untrusted source, since that is where the risk is —
      the current --input example starts from already-populated $title / $description and, copied
      literally, sends empty values or fails under set -u.
    • Note that issue text is data: it may name a repository or a command, and neither selects what the
      agent then runs.

    Acceptance criteria 2–4 are satisfied by the upstream change once submitted, and cannot be satisfied
    here — which is the finding, not a gap in the work.

    Part of #116.

  2. added theissue type on Aug 21, 2026
  3. moved this to 📥 Backlog in 🌊 Project Boardon Aug 21, 2026
  4. devantler commented on Aug 25, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    blocked label removed — "needs a decision" is not a blocker

    Part of the sweep in monorepo#3052. This
    issue carried blocked with no **Blocker:** line, so every run skipped it with nothing
    re-verifiable, and a label does not expire the way a claim does.

    Checked: 0 entries in dependencies/blocked_by, and no external project, release, or third
    party is named as gating it. What the body actually says is that a carrier decision has not been
    made — which is the one thing the engineering contract is explicit is not a skip reason:

    A "maintainer decision" is NOT a skip reason — don't block yourself on it. … When an issue
    feels like it needs his direction, that feeling is a cue to investigate it deeply and make the
    call yourself
    , then express the decision as a draft PR.

    So the label was converting a decision this role is supposed to make into an indefinite park.
    Removed; back in the normal queue.

    What is genuinely hard here, so the next run does not re-derive it

    The difficulty is real and worth stating, because it is what makes option 1 a judgement call rather
    than an obvious win:

    • The natural carrier — skills/github-issues/SKILL.md — is a synced copy owned by
      github/github-issues' upstream. Verified at pin d3c5067:
      .claude/scripts/skill-owner.sh resolves it to a non-local upstream, so an edit there is reverted
      by update-agent-skills with no conflict and no CI failure.
    • Option 1 (a plugin-authored skill) would introduce the first locally-authored skill into a
      marketplace every agent install consumes. At the pin, none of the six bundled skills is
      LOCAL — five are devantler-tech/agent-skills-synced and one is third-party — so this is a
      genuine first, and the blast radius is the reason to think before taking it.
    • Option 3 (upstream it) is the durable fix and is the one thing here that is gated: it needs the
      professional-work boundary clearance and the ask-before-upstream-creates approval, both maintainer
      acts.

    That last point is worth separating: option 3 is blocked, the issue is not. Options 1, 2 and 4
    are all available to this role, and the acceptance criterion asks only that a carrier decision be
    recorded with its trade-off — which is reachable without any maintainer action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Docs.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions