Open-source project
anchore/quill avatar
anchore/quill

quill signs and notarizes macOS binaries from a Linux CI runner

Simple mac binary signing from any platform

522 stars23 forksGoApache-2.0

At a glance

What is it?
quill is an Anchore tool that replaces the mac codesign utility for simple cases, so a Go or Rust project can sign and notarize a macOS binary from any platform, including a Linux CI worker. It also handles application bundles, with a few documented gaps.
Who is it for?
quill suits a Go or Rust project shipping macOS binaries from a Linux CI runner, or anyone who needs codesign behaviour reproducible outside a Mac. It does not suit a project with nested app bundles or helper tools that need their own entitlements, both documented as unsupported.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

codesign is a Mac-only tool, and CI runners are not Macs

The mac codesign utility is great, but it is not available on all platforms, and the README says that for cross-platform toolchains like golang this can get painful in subtle ways.

The pain is concrete. A Go project that ships binaries for macOS has to sign and notarize them, and if its release pipeline runs on Linux, the options are to run a Mac runner, or to build a cross-platform signer. Goreleaser is called a great one-shot release solution, but the README notes the catch: requiring running on a mac just for the signing.

quill is the answer to that. It is simple mac binary signing and notarization from any platform, replacing the codesign utility for simple use cases. The whole demo is one command:

bash
$ quill sign-and-notarize --p12 [path-to-p12] [path-to-unsigned-binary]

And the design constraint that makes it possible is the design constraint of the problem. Apple's notary service is an HTTP API, and the signing operation is a matter of manipulating a Mach-O binary and attaching a signature. Neither of those needs a Mac, and the only part that genuinely did need one was the keychain access, which is why one of quill's subcommands explicitly still requires a Mac.

The project sits in the Anchore organisation, alongside the go-macholibre library that does the low-level Mach-O work, which tells you the binary manipulation is not a thin wrapper around Apple's own tooling but a real implementation.

Signing takes a P12 file and a password, both from the environment

The first thing you need is the signing private key and certificate from Apple, in the form of a .p12 file. Then you set two environment variables and run the sign command:

bash
$ export QUILL_SIGN_P12=[path-to-p12]         # can also be base64 encoded contents instead of a file path
$ export QUILL_SIGN_PASSWORD=[p12-password]

$ quill sign [path/to/binary]

Two details in that snippet matter. QUILL_SIGN_P12 accepts either a path or base64 encoded contents, which is what makes it usable in a CI secret store, where a multi-line binary blob is easier to handle as a base64 string than as a mounted file. And QUILL_SIGN_PASSWORD is passed separately rather than being baked into the file, so the password can live in a different secret from the key.

Notarization works the same way, against Apple's notary service, with three more environment variables:

bash
$ export QUILL_NOTARY_KEY=[path-to-private-key-file-from-apple]   # can also be base64 encoded contents instead of a file path
$ export QUILL_NOTARY_KEY_ID=[apple-private-key-id]               # e.g. XS319FABCD
$ export QUILL_NOTARY_ISSUER=[apple-notary-issuer-id]             # e.g. a1234b5-1234-5f5d-b0c8-1234bedc5678

$ quill notarize [path/to/binary]

The notary key, its id and the issuer id are the three pieces Apple issues for the notary service, and QUILL_NOTARY_KEY accepts base64 contents for the same CI reason. The one-step form is sign-and-notarize, which takes the same path argument as the other two.

Everything is driven by environment variables rather than flags, which is the right shape for a CI tool. It also means the credentials never appear in a command line, where they would land in shell history and in process listings.

Application bundles work, with two documented gaps

quill also handles application bundles, which are .app directories, and you point it at the bundle the same way:

bash
$ quill sign [path/to/My.app]

The README describes what happens in some detail, and the detail is where the real behaviour lives. When signing a bundle, quill signs any nested binaries it finds, for example helper executables and dylibs, seals all bundle resources into Contents/_CodeSignature/CodeResources, and signs the main executable with the bundle's Info.plist and resource seal bound into its signature, using CFBundleIdentifier as the default signing identity.

That resource seal is the part that a naive implementation gets wrong. A signed bundle is not just a signed executable; the resources have to be sealed and that seal has to be bound into the signature of the main binary, or macOS will treat the bundle as tampered. Doing that from Linux means quill is reimplementing a nontrivial part of the code signing format.

When notarizing, the bundle is automatically zipped for submission to Apple's notary service, since that is what the service expects to receive.

The two gaps are stated plainly. Bundles that contain nested bundles, for example frameworks or nested .app bundles, are not supported yet. And nested binaries are signed without entitlements, because entitlements only apply to the main executable, so a nested helper that needs its own entitlements, such as a privileged helper tool, is not yet supported.

That second one is the limitation to check against your own product. If you ship a Mac app with a privileged helper, quill will sign the helper, but it will not give it the entitlements that helper needs, and you will not find out from quill.

The full certificate chain is the failure you will hit first

Notarization fails in a specific and confusing way if the certificate chain is incomplete, so it is worth reading this section before the first release rather than after it.

In order to pass notarization with Apple you must use a signing certificate that is issued by Apple, and you must have the full certificate chain available at signing time. Without the full chain, Apple rejects the notarization request, and the error comes back with two issues: the signature of the binary is invalid, and the signature does not include a secure timestamp.

quill handles this two ways. The first is that the Apple root and intermediate certificates are embedded into the Quill binary itself, obtained from Apple directly, and quill attaches the chain at signing time. The second is that you can attach the full chain to your P12 file instead, with the p12 attach-chain command:

bash
# run on a mac if you want to use certs from your keychain.
# otherwise this will embed any matching Apple certs that are found within Quill into the P12 file.

$ export QUILL_P12_PASSWORD=[p12-password]

$ quill p12 attach-chain [path-to-p12-from-apple]

# a new P12 file was created with the suffix `-with-chain.p12`

Note the constraint in that comment. p12 attach-chain MUST run on a mac with keychain access, because it reads certificates from the keychain. The command produces a new P12 file with the suffix -with-chain.p12, so the original is left alone. And quill p12 describe confirms the full certificate chain is attached, which is the check to run before you wire anything into a release pipeline.

So the practical sequence is: obtain the P12 from Apple, run p12 attach-chain on a Mac once, confirm with p12 describe, and use the -with-chain.p12 file from then on. The embedded-certificate path avoids the Mac entirely for the signing step, but it is baked into the quill binary you happen to have installed.

The goreleaser integration is where this tool earns its keep

The README shows a goreleaser configuration that ties quill into the release, and it is the most instructive part of the documentation.

yaml
# .goreleaser.yml
builds:
  - binary: my-app
    goos:
      - darwin
    goarch:
      - amd64
      - arm64
    hooks:
      post:
        # The binary is signed and notarized when running a production release, but for snapshot builds notarization is
        # skipped and only ad-hoc signing is performed (not cryptographic material is needed).
        #
        # note: environment variables required for signing and notarization (set in CI) but are not needed for snapshot builds
        #    QUILL_SIGN_P12, QUILL_SIGN_PASSWORD, QUILL_NOTARY_KEY, QUILL_NOTARY_KEY_ID, QUILL_NOTARY_ISSUER
        - cmd: quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
          env:
            - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log

Two flags carry the whole design. The --dry-run and --ad-hoc flags are wired to goreleaser's IsSnapshot template variable, so a snapshot build skips notarization and performs only ad-hoc signing, which needs no cryptographic material. A release build does the real thing. That means the same pipeline produces a nightly snapshot from a fork without any Apple credentials, and a release with them.

The comment on the environment variables lists the five that CI must set for a real release and notes that they are not needed for snapshot builds, which is exactly the kind of separation that makes this usable in an open source project that forks regularly.

The QUILL_LOG_FILE variable in the env block sends the log to a per-target file, and the -vv flag raises verbosity, so the notarization step leaves a record you can attach to a release when something goes wrong.

Also note that the build matrix is darwin on amd64 and arm64, so a single hook signs both the Intel and the Apple Silicon artifact from one Linux runner.

Submission inspection, certificate extraction, and the rest of the command set

The full command list is documented, and three of the entries are worth calling out because they exist for debugging rather than for the happy path.

The submission group covers Apple's notary service directly. submission list lists previous submissions, submission logs fetches the logs for an existing submission, and submission status checks against Apple's Notary service to see the status of a notarization submission request. When a notarization fails, these three are how you find out why without re-running the whole build, and since the notary service is asynchronous, status is a command you will genuinely use.

The inspection group is about the binary. describe shows the details of a mac binary, and extract certificates extracts certificates from a signed mac binary, which is how you confirm that what you shipped actually has the chain you expected.

The p12 group is about the key material. p12 attach-chain attaches the full Apple certificate chain into a p12 file, and p12 describe describes the contents of a p12 file.

The three primary commands are sign for a mac executable binary or application bundle, notarize for a signed binary or bundle with Apple's Notary service, and sign-and-notarize for both in one step.

Configuration is a YAML file searched in a defined order: .quill.yaml, quill.yaml, .quill/config.yaml, ~/.quill.yaml, ~/quill.yaml, and $XDG_CONFIG_HOME/quill/config.yaml. The example configuration is small, with a log section only:

yaml
log:
  # suppress logging output (env var: "QUILL_LOG_QUIET")
  quiet: false
  
  # error, warn, info, debug, trace (env var: "QUILL_LOG_LEVEL")
  level: "info"
  
  # file to write all loge entries to (env var: "QUILL_LOG_FILE")
  file: ""

Each key has a matching environment variable, so the log file path can be set per CI target without a config file at all, which is what the goreleaser example does.

Where quill is the wrong tool

The first wrong-tool case is a product with nested bundles or privileged helpers. Both gaps are documented. If you ship a .app that contains frameworks, or a helper tool that needs its own entitlements, quill will not sign them correctly, and the failure shows up on the user's machine rather than in your build log.

The second case is a project that needs the full codesign feature set. quill describes itself as replacing the codesign utility for simple use cases. The commands it does not have are the ones codesign has for keychain integration, entitlements management, requirements and test signing, and for the finer points of how a signature is constructed. If your release needs those, you need a Mac and the real tool.

The third case is a non-macOS platform. Everything here is about Mach-O binaries, Apple certificates and Apple's notary service, and the command list is entirely about mac executables and app bundles.

For the alternative approach, the difference in approach is between reimplementing the signing format and driving the real tool. Running a macOS CI runner and invoking codesign natively gives you the complete Apple behaviour, including the parts quill has not implemented, at the cost of a macOS runner, which is slower and more expensive to rent than a Linux one. A third option is a signing service that takes care of the credentials and returns a signed artifact, which moves the trust boundary to a third party and is a reasonable choice for a team that does not want the P12 in its CI at all.

The quill approach sits in the middle: a real implementation of the format, runnable anywhere, with the Mac required only for the one keychain-dependent step that needs it, which is a one-time preparation of the P12 file rather than a per-build requirement.

Apache-2.0, frequent releases, and a Go project with real dependencies

The licence is Apache-2.0, the permissive choice with an explicit patent grant, which is what Anchore uses across its projects.

The release history is regular. The recent releases are v0.7.1 on 2026-03-10, v0.7.0 on 2026-03-09 and v0.5.1 on 2025-02-10, and the last push to the repository was on 2026-10-16. The version is still below 1.0, which for a CLI tool that people script is worth noting, since flag names and environment variable names can change between minor versions.

The dependency list in go.mod is the best guide to what the tool actually does. The signing side is github.com/anchore/go-macholibre, the project's own Mach-O library, plus github.com/blacktop/go-macho for parsing Mach-O files, and software.sslmate.com/src/go-pkcs12 for reading the P12 key material. The certificate side is howett.net/plist for the Info.plist handling that the bundle signing needs, and github.com/github/smimesign for the signature envelope format.

The notarization side is the AWS SDK for Go v2, with the S3 service and the transfermanager feature, which is how a notarization submission is uploaded when the binary is too large to post inline, and the credentials package for the temporary credentials the notary flow uses.

Around that is a CLI stack: cobra for commands, bubbletea and lipgloss for the terminal interface, clio and fangs from Anchore for the command framework and its help output, go-logger for logging, and go-progress for the progress display during a long upload.

The module requires Go 1.26.0. The repository also carries a .goreleaser.yaml, a .golangci.yaml, a Makefile that delegates everything to a Go program in the .make/ directory, an llms.txt file, DEVELOPING.md and RELEASE.md, and a COPYRIGHT file.

Editorial conclusion

quill suits a Go or Rust project shipping macOS binaries from a Linux CI runner, or anyone who needs codesign behaviour reproducible outside a Mac. It does not suit a project with nested app bundles or helper tools that need their own entitlements, both documented as unsupported. Before adopting, check whether your signing certificate already carries the full Apple chain, since Apple rejects notarization otherwise, and run quill p12 describe to confirm the chain is attached before wiring quill into a goreleaser post hook.

Frequently asked questions

How do I install quill?

Run `curl -sSfL https://get.anchore.io/quill | sudo sh -s -- -b /usr/local/bin`. You can also pass a destination directory and a release version to the same script with -b <DESTINATION_DIR> <RELEASE_VERSION>.

Which environment variables does quill need to sign a binary?

QUILL_SIGN_P12 for the path or base64 contents of the .p12 file, and QUILL_SIGN_PASSWORD for its password. For notarization you also need QUILL_NOTARY_KEY, QUILL_NOTARY_KEY_ID and QUILL_NOTARY_ISSUER.

Why does Apple reject my notarization request with quill?

Usually because the full certificate chain was not available at signing time. Apple then reports that the signature of the binary is invalid and that it does not include a secure timestamp. You can attach the chain with `quill p12 attach-chain` on a mac, then confirm with `quill p12 describe`.

Does quill support .app bundles with frameworks or privileged helpers?

Not yet. Bundles containing nested bundles, such as frameworks or nested .app bundles, are not supported, and nested binaries are signed without entitlements, so a nested helper needing its own entitlements is not supported either.

Official sources

  1. anchore/quill on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/anchore-quill.svg)](https://hysenlabs.com/projects/anchore-quill)

Was this useful?