# Release, signing, and notarization

> The goreleaser pipeline, the `sign.sh` / `notarize.sh` / `release-tarball.sh` scripts, Developer ID signing for `teamid:` partition trust, and CI secret renewal.

- Repository: mvanhorn/agentcookie
- GitHub: https://github.com/mvanhorn/agentcookie
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-agentcookie-137da38edfae/llms-full.txt

## Source Files

- `.goreleaser.yaml`
- `scripts/sign.sh`
- `scripts/notarize.sh`
- `scripts/release-tarball.sh`
- `docs/runbook-v0.12-codesign.md`
- `docs/runbook-v0.12-security-hardening.md`

---

---
title: "Release, signing, and notarization"
description: "The goreleaser pipeline, the `sign.sh` / `notarize.sh` / `release-tarball.sh` scripts, Developer ID signing for `teamid:` partition trust, and CI secret renewal."
---

`make release` is the single command that builds, Developer-ID signs, and Apple-notarizes `bin/agentcookie` on macOS. It shells through three scripts under `scripts/`: `sign.sh` (codesign with Hardened Runtime + secure timestamp), `notarize.sh` (xcrun notarytool round-trip), and `release-tarball.sh` (closed-beta bundle wrapping the notarized binary with the install script and quickstart). GoReleaser drives the same `sign.sh` post-hook on darwin/arm64 and darwin/amd64 cross-builds, then the `release` workflow attaches the beta tarball to the GitHub release. The default signing identity is `Developer ID Application: Matthew Charles Van Horn (NM8VT393AR)`; that Team ID is the linchpin that flows into Chrome Safe Storage's `teamid:NM8VT393AR` partition entry for universal cookie delivery.

## Pipeline at a glance

```text
make release
├─ make build           go build -o bin/agentcookie ./cmd/agentcookie
├─ make sign            scripts/sign.sh bin/agentcookie
│                       └─ codesign --options runtime --timestamp --sign "$IDENTITY"
│                          codesign --verify --deep --strict
└─ make notarize        scripts/notarize.sh bin/agentcookie
                        ├─ ditto -c -k --keepParent  (zip wrap)
                        └─ xcrun notarytool submit --wait --keychain-profile

scripts/release-tarball.sh v0.X.Y      (NOT part of make release)
└─ dist/agentcookie-<ver>-darwin-arm64.tar.gz
   ├─ agentcookie         (notarized)
   ├─ install-beta.sh
   └─ quickstart-beta.md

GoReleaser (CI, tagged push)
├─ darwin/arm64 + darwin/amd64 builds, CGO_ENABLED=1
├─ post-hook: scripts/sign.sh {{ .Path }}        on each binary
└─ archives: agentcookie_<ver>_darwin_<arch>.tar.gz + checksums.txt
```

## Build matrix and ldflags

`.goreleaser.yaml` defines one build (`id: agentcookie`) targeting `darwin/arm64` and `darwin/amd64` with `CGO_ENABLED=1` (required because the Keychain bridge in `internal/chrome` uses Security.framework). The `Version` symbol is set via `-ldflags`:

```yaml
ldflags:
  - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }}
```

The matching variable lives at `internal/cli/root.go` (`var Version = "0.0.1-dev"`). Local `make build` does not pass `-ldflags`, so a dev binary reports `0.0.1-dev`; goreleaser builds report the tag.

GoReleaser archive contents (`archives[].files`):

```text
LICENSE
README.md
docs/quickstart.md
docs/threat-model.md
docs/architecture.md
docs/protocol.md
docs/faq.md
examples/source.yaml
examples/sink.yaml
examples/allowlist.yaml
examples/launchd-sink.plist
```

The signing post-hook runs once per binary, before archive assembly:

```yaml
hooks:
  post:
    - cmd: scripts/sign.sh {{ .Path }}
      output: true
```

## scripts/sign.sh

Signs each binary in place with Developer ID + Hardened Runtime + secure timestamp. `--force` lets it re-sign over a previous signature, so the same script covers fresh `go install` output and the goreleaser build path.

```bash
codesign \
  --force \
  --options runtime \
  --timestamp \
  --sign "$IDENTITY" \
  "$binary"
codesign --verify --deep --strict --verbose=2 "$binary"
```

<ParamField path="AGENTCOOKIE_SIGN_IDENTITY" type="env">
codesign identity (CN or SHA-1 fingerprint). Default: `Developer ID Application: Matthew Charles Van Horn (NM8VT393AR)`. Contributors override with their own Developer ID for fork builds.
</ParamField>

Preflight check: `security find-identity -v -p codesigning | grep -qF "$IDENTITY"`. If the identity is absent, the script exits **2** with a pointer to `docs/runbook-v0.12-codesign.md`. Other exits: `1` usage, `3` codesign/verify failed.

The signature uses a stable designated requirement that does **not** include the cdhash, so every rebuild produces a byte-identical requirement that the wizard install's per-binary Keychain ACL continues to match:

```text
identifier "agentcookie" and anchor apple generic
  and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */
  and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */
  and certificate leaf[subject.OU] = NM8VT393AR
```

## scripts/notarize.sh

Wraps the binary in a zip (notarytool rejects bare Mach-O), submits via `xcrun notarytool submit --wait`, and requires a JSON status of `Accepted`. Single Mach-O binaries are intentionally **not stapled** — stapling targets bundles, dmgs, and installers. Gatekeeper performs an online check at first launch against the accepted record.

```bash
ditto -c -k --keepParent "$binary" "$tmpzip"
xcrun notarytool submit "$tmpzip" \
  --keychain-profile "$PROFILE" \
  --wait \
  --output-format json > /tmp/notary-result.json
grep -q '"status": *"Accepted"' /tmp/notary-result.json
```

<ParamField path="AGENTCOOKIE_NOTARY_PROFILE" type="env">
Name of the `xcrun notarytool store-credentials` profile in the login keychain. Default `agentcookie-notary`.
</ParamField>

Exit codes: `0` accepted, `1` usage, `2` profile missing, `3` Apple rejected, `4` staple failed (reserved; not exercised today).

### One-time notary credentials setup

```bash
xcrun notarytool store-credentials agentcookie-notary \
  --apple-id mvanhorn@gmail.com \
  --team-id NM8VT393AR \
  --password <app-specific-password>
```

The app-specific password is created at `appleid.apple.com` → Sign-In and Security → App-Specific Passwords. notarytool persists it in the login keychain; cleartext lives nowhere else.

## scripts/release-tarball.sh

Produces the closed-beta bundle that friends extract and run. Not part of `make release` — CI runs `make release` first, then this script.

```text
dist/agentcookie-<version>-darwin-arm64.tar.gz
└─ agentcookie-<version>-darwin-arm64/
   ├─ agentcookie         (notarized, +x)
   ├─ install-beta.sh     (+x)
   └─ quickstart-beta.md
```

Preflight: `bin/agentcookie`, `scripts/install-beta.sh`, `docs/quickstart-beta.md` must exist; `codesign -d -r- bin/agentcookie` must return a signature (notarization is left to spctl on the consumer Mac). Architecture is derived from `uname -m` (`arm64` → `darwin-arm64`, otherwise `darwin-<arch>`). The script prints a SHA-256 for inclusion in release notes.

The consumer side, `install-beta.sh`, re-verifies the signature with `codesign -d -r-` and grep-asserts `subject.OU. = NM8VT393AR` before placing the binary. Mismatch produces a warning, not a hard fail; an unsigned/unverifiable binary continues with a Gatekeeper warning.

## Why Developer ID matters: `teamid:` partition trust

Universal cookie delivery on the sink writes the synced session into Chrome's real `Default` profile by reading Chrome Safe Storage. v0.13 grants that access with a single non-destructive partition update:

```bash
security set-generic-password-partition-list \
  -S "apple-tool:,apple:,teamid:<TEAM_ID>" \
  -k "<login-password>" \
  -s "Chrome Safe Storage" -a Chrome
```

The `<TEAM_ID>` is resolved from agentcookie's own code signature at install time. `internal/chrome/keychain.go` composes it:

```go
// TeamPartitionList composes the partition string ...
// (teamid:<teamID>). The teamid entry is what covers agentcookie's own
// CGO read path (SecItemCopyMatching) and any tool the operator signs
// with the same Developer ID team.
return DefaultPartitionList + ",teamid:" + teamID
```

Implications for the release pipeline:

| Partition entry | What it grants | Source |
|---|---|---|
| `apple-tool:` | `/usr/bin/security` reads (covers `yt-dlp`, `pycookiecheat`, `browser_cookie3`, `gallery-dl`) | Apple-shipped |
| `apple:` | Apple-signed system binaries | Apple-shipped |
| `teamid:NM8VT393AR` | Any binary signed with the maintainer's Developer ID team | `scripts/sign.sh` output |

An **unsigned** agentcookie binary (e.g. a contributor build without a Developer ID) does not benefit from the `teamid:` entry on the sink. CGO reads via `SecItemCopyMatching` will then be rejected. This is why `make sign` is part of the default `make` target and why the goreleaser post-hook is not optional.

## GitHub Actions release workflow

`.github/workflows/release.yml` runs on `push` of tags matching `v*` on `macos-latest`. The job is gated by the repo variable `RELEASE_CI_ENABLED == 'true'`; until that is flipped, releases are cut locally.

<Steps>
<Step title="Import the Developer ID cert">
`apple-actions/import-codesign-certs@v3` decodes the base64 `.p12` from `secrets.CERTIFICATE_OSX_APPLICATION` (unlocked with `CERTIFICATE_OSX_APPLICATION_PASSWORD`) into an ephemeral keychain added to the search list.
</Step>
<Step title="List identities">
`security find-identity -v -p codesigning` runs as a sanity check; the run log shows the NM8VT393AR identity when the secret is set correctly.
</Step>
<Step title="Provision notary credentials">
If `AC_NOTARY_PASSWORD` is set, `xcrun notarytool store-credentials agentcookie-notary --apple-id mvanhorn@gmail.com --team-id NM8VT393AR --password "$AC_NOTARY_PASSWORD"` writes the profile into the runner's login keychain.
</Step>
<Step title="Build, sign, notarize">
`make release` produces `bin/agentcookie` signed and accepted by Apple.
</Step>
<Step title="Build the beta bundle">
`scripts/release-tarball.sh "${{ github.ref_name }}"` writes `dist/agentcookie-<ref>-darwin-arm64.tar.gz`.
</Step>
<Step title="Run goreleaser">
`goreleaser/goreleaser-action@v6` with `args: release --clean`, exporting `AGENTCOOKIE_SIGN_IDENTITY` so the build post-hook signs every darwin/arch binary with the same identity.
</Step>
<Step title="Attach the beta bundle">
`gh release upload "${{ github.ref_name }}" dist/agentcookie-*-darwin-arm64.tar.gz --clobber` adds the closed-beta tarball alongside goreleaser's per-arch archives.
</Step>
</Steps>

## CI secrets reference

| Secret / variable | Kind | Used by | Notes |
|---|---|---|---|
| `CERTIFICATE_OSX_APPLICATION` | Secret | `apple-actions/import-codesign-certs@v3` | base64 of the `.p12` containing cert + private key. |
| `CERTIFICATE_OSX_APPLICATION_PASSWORD` | Secret | same | Password for the `.p12`. |
| `AC_NOTARY_PASSWORD` | Secret | `xcrun notarytool store-credentials` | App-specific password from `appleid.apple.com`. |
| `RELEASE_CI_ENABLED` | Variable | Job-level `if:` | Set to `true` to enable CI-driven releases; otherwise the job is a no-op so tagged pushes do not pollute history. |
| `GITHUB_TOKEN` | Auto | `goreleaser-action`, `gh release upload` | Standard Actions-provided token; no manual setup. |

## Secret renewal and lifecycle

### Developer ID Application cert (5-year validity)

A renewed cert from the Apple Developer portal keeps the same Team ID and the same Common Name shape. Because `scripts/sign.sh` emits a designated requirement that asserts only `subject.OU = NM8VT393AR` and Apple's anchor, **a renewed cert produces the identical designated requirement** — existing per-binary Keychain ACLs and the `teamid:NM8VT393AR` partition entry continue to match. No re-trust pass on sink machines is required.

<Steps>
<Step title="Issue the new cert">
Apple Developer portal → Certificates → new Developer ID Application. Provide a CSR from a fresh private key (Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority).
</Step>
<Step title="Install on the build Mac">
Double-click the downloaded `.cer` to bind it to the new private key in the login keychain.
</Step>
<Step title="Export the .p12">
Keychain Access → expand the disclosure triangle so the export includes both the cert and its private key → Export 2 items → `.p12` with a strong password.
</Step>
<Step title="Rotate the CI secret">
<CodeGroup>
```bash gh CLI
base64 -i agentcookie-codesign.p12 -o agentcookie-codesign.p12.b64
gh secret set CERTIFICATE_OSX_APPLICATION < agentcookie-codesign.p12.b64
gh secret set CERTIFICATE_OSX_APPLICATION_PASSWORD
rm agentcookie-codesign.p12 agentcookie-codesign.p12.b64
```
</CodeGroup>
</Step>
<Step title="Revoke the old cert">
Optional, once one tagged release has succeeded with the new cert.
</Step>
</Steps>

<Warning>
Delete the local `.p12` and its base64 copy after upload. Never commit either to the repo.
</Warning>

### Notarytool app-specific password

Apple does not auto-expire app-specific passwords, but rotate on user role changes or compromise.

```bash
# 1. Generate a new password at https://account.apple.com → App-Specific Passwords
# 2. Update CI
gh secret set AC_NOTARY_PASSWORD
# 3. Update the local login keychain
xcrun notarytool store-credentials agentcookie-notary \
  --apple-id mvanhorn@gmail.com \
  --team-id NM8VT393AR \
  --password <new-password>
# 4. Revoke the old password from appleid.apple.com
```

## Verifying a release locally

```bash
security find-identity -v -p codesigning
# Expect: one line containing "NM8VT393AR"

make build && codesign -d -r- bin/agentcookie > /tmp/req1
make clean && make build && codesign -d -r- bin/agentcookie > /tmp/req2
diff /tmp/req1 /tmp/req2          # empty: byte-stable designated requirement

make verify                       # prints the designated requirement
# Last line: subject.OU = NM8VT393AR
```

For a release candidate, verify the bundle the way `install-beta.sh` will:

```bash
codesign --verify --strict --verbose=2 bin/agentcookie
codesign -d -r- bin/agentcookie | grep 'subject.OU. = NM8VT393AR'
spctl --assess --type install bin/agentcookie     # info-only; CLI binaries are not "apps"
xcrun notarytool history --keychain-profile agentcookie-notary | head
```

## Troubleshooting

<AccordionGroup>
<Accordion title="codesign: errSecInternalComponent">
The login keychain is locked, or the LaunchAgent/SSH session has no access. From the GUI user session: `security unlock-keychain login.keychain-db`, then re-run `make sign`.
</Accordion>
<Accordion title="sign.sh exit 2: identity not available">
`security find-identity -v -p codesigning` returned nothing matching the configured identity. Re-import the `.p12` (with private key) into the login keychain, or override via `AGENTCOOKIE_SIGN_IDENTITY` for a contributor build.
</Accordion>
<Accordion title="The timestamp service is not available">
Transient outage at `timestamp.apple.com`. Retry. Do not drop `--timestamp`; notarization requires it.
</Accordion>
<Accordion title="notarize.sh exit 2: notary profile not found">
The login keychain has no `agentcookie-notary` profile (or `$AGENTCOOKIE_NOTARY_PROFILE`). Run `xcrun notarytool store-credentials agentcookie-notary --apple-id mvanhorn@gmail.com --team-id NM8VT393AR --password <app-specific>`.
</Accordion>
<Accordion title="Apple rejected the submission (exit 3)">
Inspect with `xcrun notarytool log <submission-id> --keychain-profile agentcookie-notary`. Common causes: Hardened Runtime missing (re-run `make sign`), timestamp missing (re-run `make sign`), an unsigned framework (not applicable to the single-file binary).
</Accordion>
<Accordion title="CI passes locally but fails with 'identity not found'">
The `CERTIFICATE_OSX_APPLICATION` secret is unset or the imported `.p12` lacks the private key. Keychain Access must show the private key under the cert's disclosure triangle before re-exporting.
</Accordion>
<Accordion title="release-tarball.sh: missing bin/agentcookie">
The script is intentionally separate from `make release`. Run `make release` first, then `scripts/release-tarball.sh <version>`.
</Accordion>
<Accordion title="install-beta.sh warns 'Developer ID OU does not match NM8VT393AR'">
The consumer downloaded a fork build or an unsigned build. Install continues, but on the sink the `teamid:NM8VT393AR` partition entry will not grant Safe Storage access to the new binary; universal cookie delivery downgrades to sidecar/adapter delivery.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
<Card title="Universal cookie delivery" href="/universal-delivery">
How the `teamid:` partition entry, produced by the Developer ID signature here, lets unmodified cookie tools read the synced Chrome Default profile.
</Card>
<Card title="LaunchAgent management" href="/launchagent-management">
Why a stable, byte-identical designated requirement matters for the per-binary Keychain ACL that survives every `go install` cycle.
</Card>
<Card title="doctor and adapter verification" href="/doctor-health-checks">
Post-install signals (`Cookie delivery: universal`, master-key presence) that confirm the signed/notarized binary is trusted on the sink.
</Card>
<Card title="Headless second-Mac install" href="/headless-install">
The SSH-only install path that depends on the one-password partition update unlocked by Developer ID trust.
</Card>
</CardGroup>
