Material OSS público. Os repositórios citados (
yolo-labz/claude-mac-chrome,yolo-labz/wa,yolo-labz/kokoro-speakd,yolo-labz/claude-classroom-submit,yolo-labz/linkedin-chrome-copilot,yolo-labz/fand) estão todos abertos no GitHub. Os números a seguir saíram dos pipelines de release que estão rodando hoje.
O artefato é pequeno; a confiança que ele exige, não
Um plugin do Claude Code não passa de um artefato minúsculo: um hook script, um slash command, um wrapper de servidor MCP. O repositório acompanha esse tamanho — a mediana dos seis em questão fica em ~2 KLOC. Só que a superfície de confiança não cabe nesse mesmo molde. O plugin é carregado dentro da mesma sessão de shell onde vivem o código, os segredos e os tokens de quem desenvolve. A partir daí, um tarball sem assinatura deixa de ser detalhe e vira superfície de ataque legítima.
E aqui está o detalhe que muda tudo: até abril de 2026, o marketplace de plugins do Claude Code da Anthropic não cobra absolutamente nada nesse front. Nada de assinatura, nada de SBOM, nada de SLSA, nenhuma checagem de assinatura na hora de instalar. A confiança é depositada no marketplace, nunca no plugin individual. Ou seja: blindar a cadeia de suprimentos desses repositórios é trabalho voluntário, feito antes de a política do marketplace exigir. Vale a pena assim mesmo por dois motivos. Primeiro, quem sabe o que está olhando consegue verificar o plugin com um comando só. Segundo, no dia em que o marketplace resolver apertar as regras, a frota já chega pronta.
A decisão, em uma frase
No contexto de um dev solo publicando plugins OSS, diante da pressão por confiança na cadeia de suprimentos e sem nenhum time de segurança por trás, decidimos por attestations SLSA L2 + cosign keyless via OIDC + SBOM duplo, tudo aplicado por um único cânone de GitHub Actions, para obter provenance em nível de auditoria sem custo por plugin — aceitando, em troca, o custo único de montar o template.
Seis repositórios, três formatos, um cânone só
O critério para entrar na lista foi simples: todo repositório precisa ter uma superfície de release — um binário, uma wheel ou um tarball com tag. Os seis que se encaixam:
| Repositório | Linguagem | O que vai pra release |
|---|---|---|
claude-mac-chrome | Bash | tarball com tag + Homebrew tap |
wa | Go 1.24 | binários darwin/linux via GoReleaser + Homebrew tap |
kokoro-speakd | Python 3.12 | wheel no PyPI + pesos do modelo no GitHub Release |
claude-classroom-submit | Python 3.12 | wheel no PyPI |
linkedin-chrome-copilot | Bash | tarball com tag |
fand | Rust | binários compilados com cargo + Homebrew tap |
São três formatos de release distintos — binário, wheel e tarball — convivendo sob o mesmo cânone. E a tarefa do cânone é exatamente essa: fazer com que a superfície de assinatura fique idêntica nos três, sem importar o que está embaixo.
O coração: nove linhas que entram em todo lugar
O passo de assinatura que cai em cada workflow de release é curto a ponto de caber numa olhada:
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: ${{ inputs.artifact-path }}
- name: Attest SBOM
uses: actions/attest-sbom@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: ${{ inputs.artifact-path }}
sbom-path: sbom.cdx.jsonDuas actions nativas do GitHub, pinadas pelo SHA completo de 40 caracteres com aquele comentário # v4.1.0 no fim — o pedaço de que a regex do Dependabot depende para reconhecer a entrada. E não há nenhuma infraestrutura de assinatura externa no caminho. O token OIDC emitido por https://token.actions.githubusercontent.com é trocado, na CA Fulcio do Sigstore, por um certificado de assinatura de vida curta; a assinatura é registrada no log de transparência público Rekor; e o bundle que sai disso vai anexado ao GitHub Release.
Vale parar num tropeço clássico. O issuer OIDC do CI é https://token.actions.githubusercontent.com, e ponto. A URL https://github.com/login/oauth é outra coisa — é o fluxo OAuth interativo, feito por gente. Apontar essa segunda na flag --certificate-oidc-issuer de um workflow rende um erro nada óbvio lá no cosign verify-blob. Então, se aparecer qualquer reclamação de issuer-mismatch, comece conferindo essa string.
Dois SBOMs numa chamada só do syft
Dois formatos importam aqui, e por razões regulatórias bem concretas. O CycloneDX 1.7 é o padrão da OWASP e é o que o EU Cyber Resilience Act cobra dos produtos em escopo. O SPDX 2.3 é o padrão ISO/IEC 5962:2021, exigido pela US Executive Order 14028 nas aquisições federais. Houve um tempo em que produzir os dois significava rodar duas vezes; o syft de hoje resolve numa invocação única:
syft . \
-o [email protected]=sbom.cdx.json \
-o spdx-json=sbom.spdx.jsonNo repositório Go (wa), uma rodada extra de cyclonedx-gomod app -licenses -std -json agrega um SBOM nativo de Go bem mais detalhado — versões de módulo, resumos de licença, as fronteiras da std-lib — que se soma ao que o syft já produziu. Já os repositórios Python apoiam-se no pypa/gh-action-pypi-publish@release/v1 (v1.11+), que gera as attestations PEP 740 sozinho, a partir do fluxo OIDC do Trusted Publishing, e dispensa um passo Sigstore avulso.
Quanto ao tamanho, nada disso pesa. O sbom.cdx.json fica em ~12 KB nos repositórios Bash e em ~84 KB no wa depois do augment do gomod. O bundle de provenance (attestation.intoto.jsonl) para em ~4 KB por artefato. Somando tudo, não dá nem para chamar de footprint relevante nos assets do release.
Fechando as permissões: negar por padrão
A regra no nível do workflow é negar tudo de saída e deixar cada job reconceder, na unha, só aquilo de que ele precisa:
permissions: {}
jobs:
release:
permissions:
id-token: write # OIDC for keyless signing
attestations: write # write provenance + SBOM attestations
contents: write # cut the GitHub Release
runs-on: ubuntu-latest
steps:
- uses: step-security/harden-runner@<sha> # v2
with:
egress-policy: audit
- uses: actions/checkout@<sha>
with:
persist-credentials: false
# ... build + sign stepsO step-security/harden-runner começa em egress-policy: audit no primeiro ciclo de release — o objetivo é só observar quais endpoints legítimos do Sigstore são acionados — e vira block quando a allowlist já estiver assentada. E o persist-credentials: false no actions/checkout impede que o GITHUB_TOKEN padrão fique esquecido dentro do .git/config depois que o workflow encerra.
A fitness function: verificar a cada release
Um pipeline de assinatura que nunca é exercido do lado de quem consome é um pipeline que apodrece caladinho. Por isso a fitness function mora dentro do próprio CI: todo workflow de release termina executando exatamente a verificação que ele espera que o usuário rode.
- name: Smoke-verify the just-signed artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
artifact="${{ inputs.artifact-path }}"
repo="${{ github.repository }}"
gh attestation verify "${artifact}" \
--repo "${repo}" \
--predicate-type "https://slsa.dev/provenance/v1" \
--format json \
| jq -e '.[] | select(.verificationResult.signature.certificate.sourceRepositoryURI == "https://github.com/${{ github.repository }}")'
echo "OK: SLSA L2 provenance verified against ${repo}"Se o artefato que acabou de ser publicado não bater com a própria attestation, o release quebra ali mesmo — um aviso mais alto do que qualquer linha de documentação jamais será. E não é coincidência: esse gh attestation verify é o mesmo comando que vai no README como smoke test do usuário.
A armadilha: nunca re-tagueie um release
Essa é a pegadinha que apanha o dev solo logo de cara. O CI falhou, a tag já tinha sido empurrada cedo demais, e o reflexo é apagar a tag local, dar force-push, corrigir e taguear de novo por cima.
Não faça. O slsa-verifier e o predicate de provenance por baixo dele validam contra o commit SHA do momento da assinatura. Re-taguear gera uma provenance defasada: os commits novos existem, mas a afirmação assinada continua apontando para o SHA antigo. Na prática, o gh attestation verify segue passando contra o bundle assinado da tag original, enquanto qualquer pessoa que baixar a árvore de fontes re-tagueada vai receber artefatos cuja attestation descreve um commit que ela nem tem em mãos.
A saída é entediante de tão simples: corte a vX.Y.Z+1. Sobe o número de patch, empurra a tag nova, roda o workflow outra vez e arquiva o release quebrado com uma nota “superseded by” no corpo. É essa disciplina de tags monotônicas que mantém a provenance verificável.
O que dá pra esperar do Scorecard
O OpenSSF Scorecard ajuda como termômetro, não como meta. Para um repositório solo-dev da yolo-labz, o teto realista é ~8.7/10, e quem puxa essa nota para baixo é, estruturalmente, o check de Contributors: ele trava por volta de 3/10 para quem trabalha sozinho e não dá para burlar com trailers Co-Authored-By: — o Scorecard descarta bots e ignora campos Company vazios. Fora esse, todos os demais passam de 8 assim que o cânone entra:
- Pinned-Dependencies →
10/10depois de passar cada action pelo secure-workflow rewriter da StepSecurity. - Token-Permissions →
10/10por conta do deny-all como padrão. - Signed-Releases →
10/10assim que as attestations do Sigstore estão anexadas. - Packaging →
10/10, já que todo repositório publica por uma action reconhecida (softprops/action-gh-release,pypa/gh-action-pypi-publish). - Maintained → se autorrecupera no dia 90, bastando ao menos um commit por semana.
O único check que cobra código de verdade é o de Fuzzing. Um workflow fuzz.yml é invisível para o Scorecard — ele quer ver ou fuzz tests em Go (func FuzzX(f *testing.F) em qualquer *_test.go), ou um diretório .clusterfuzzlite/ cumprindo o contrato do OSS-Fuzz. No wa, uma única função de fuzz em Go já rende dez pontos nesse quesito. Para os repositórios shell, como o claude-mac-chrome, o caminho é ClusterFuzzLite, via .github/workflows/cflite_pr.yml mais o .github/workflows/cflite_cron.yml para os batch runs noturnos.
Do outro lado do balcão: o que o usuário precisa rodar
No README, o bloco de verificação encolhe para dois comandos:
gh release download v1.2.0 --repo yolo-labz/wa
gh attestation verify wa-darwin-arm64.tar.gz --repo yolo-labz/waÉ esse o ritual inteiro para alguém estabelecer confiança e confirmar que o artefato saiu mesmo do pipeline anunciado. O cosign verify-blob e o slsa-verifier existem, mas são os caminhos offline e avançados — lugar deles é num apêndice Reproducing the verification offline, jamais na manchete. Misturar “avançado” com “padrão” é justamente por onde a UX de confiança começa a apodrecer.
A conta, sem maquiagem
A contabilidade honesta do cânone, depois de espalhá-lo pelos seis repositórios:
- Escrever o template (uma vez só): uns dois dias no primeiro repositório, já contando o vai-e-vem de depurar a egress-allowlist para o modo
blockdoharden-runner. - Plugar cada novo repositório: ~30 minutos — copiar o
release.yml, ajustar osubject-path, encaixar o job de build da linguagem e dar push. - Overhead por release: ~25 segundos a mais no CI, somando geração de SBOM + attestation + smoke verify.
- Segredos rotacionados: zero. O Sigstore keyless elimina de vez a chave de assinatura de longa duração.
O setup é um custo fixo que se dilui em cada release futuro da frota. O custo marginal de cada plugin é só a cópia do YAML — a criptografia é o mesmo trecho de código rodando sobre a mesma imagem de runner.
Referências
- SLSA Specification v1.0, Build Track Levels — slsa.dev/spec/v1.0/levels
- GitHub, Using artifact attestations to establish provenance for builds — docs.github.com/en/actions/security-guides/using-artifact-attestations
- Sigstore, Rekor Transparency Log Architecture — docs.sigstore.dev/rekor/overview
- Zahan, Lin et al. (2023) OpenSSF Scorecard: On the path toward ecosystem-wide automated security metrics
arXiv:2208.03922— arxiv.org/abs/2208.03922 - CycloneDX 1.7 spec — cyclonedx.org/specification/overview
- Linux Foundation / SPDX 2.3 — spdx.github.io/spdx-spec/v2.3
- PEP 740, Index support for digital attestations — peps.python.org/pep-0740