Sign Windows binaries from your build

Code signing happens after compile and before ship, same place you would run a packer or an installer builder. Bamboo Deploy puts that step inside the tools you already use: one CLI, a GitHub Action, an electron-builder hook, and a plain REST API. No certificate to buy, no USB token, no Windows VM.

Quick start

export BAMBOO_API_KEY=bd_live_...
npx github:bamboodeploy/cli sign dist/myapp.exe

In Windows PowerShell with scripts blocked, use npx.cmd instead of npx.

That uploads the file, runs the malware scan, signs it with a trusted certificate, and writes the signed file back in place. Works on Windows, macOS, and Linux with Node 18 or newer. Add --out signed/myapp.exe to keep the original.

Not ready to sign? Scanning is free and needs no subscription:

npx github:bamboodeploy/cli scan dist/myapp.exe

Prints the full analysis report (packer, imports, YARA, signing status) and tells you whether the build will trip SmartScreen.

API key and auto-sign

  1. Open the dashboard, go to API Keys, create a key. It starts with bd_live_ and is shown once.
  2. Export it as BAMBOO_API_KEY, or pass --api-key, or put BAMBOO_API_KEY=bd_live_... in a .bamboorc file in the project or home directory.
  3. Ask for Auto-sign. New accounts have every sign request reviewed by an operator (usually within a few hours, and your CI job will time out waiting). Once your first build has been reviewed, ask us to enable Auto-sign: builds with a clean multi-engine scan are then signed within about two minutes, no human in the loop. Flagged builds still go to review.
Signing needs a Premium subscription ($45 per quarter) and two verified publisher connections (email plus a domain, GitHub, or similar). Uploads and scans work on Free.

CLI reference

npm install -g github:bamboodeploy/cli     # or use npx each time

bamboo sign <file> [--out <path>] [--timeout 900] [--interval 10] [--json]
bamboo scan <file> [--json] [--fail-on-findings]
bamboo status [app-id]
bamboo delete <app-id>
bamboo config
FlagMeaning
--outWrite the signed file here instead of replacing the input
--timeoutSeconds to wait for signing, default 900
--jsonMachine-readable output on stdout, progress stays on stderr
--fail-on-findingsscan exits 5 when a packer or YARA match is reported
--api-key, --api-baseOverride the env var / default host

From Node, the same functions are importable:

const { signFile, scanFile } = require('@bamboodeploy/cli');
await signFile('dist/app.exe', { out: 'dist/app-signed.exe', log: console.error });

Electron electron-builder

Install the hook and point electron-builder at it. Every .exe and .msi artifact is signed in place after the build.

npm install --save-dev github:bamboodeploy/electron-builder-bamboo-sign
{
  "build": {
    "afterAllArtifactBuild": "electron-builder-bamboo-sign",
    "win": { "target": ["nsis"] }
  }
}

Using electron-updater? Sign the app exe and the installer through the win.sign hook instead, so the generated latest.yml hashes match:

{ "build": { "win": { "sign": "electron-builder-bamboo-sign/sign" } } }

Set BAMBOO_SKIP=1 to build locally without signing. Electron Forge users: add npx github:bamboodeploy/cli sign out/make/squirrel.windows/x64/*.exe to the postMake step, or run it as a script after electron-forge make.

Background reading: Why Electron apps trip Windows Defender and SmartScreen.

CI pipelines

Every CI below runs the same one-liner. Store the key as a secret, adjust the path.

GitHub Actions

- name: Sign Windows binary
  uses: bamboodeploy/sign-action@v2
  with:
    api-key: ${{ secrets.BAMBOO_API_KEY }}
    file: ./dist/myapp.exe

Runs on windows-*, ubuntu-*, and macos-* runners. Source: github.com/bamboodeploy/sign-action.

GitLab CI

sign-windows:
  stage: deploy
  image: node:20
  needs: ["build-windows"]
  script:
    - npx -y github:bamboodeploy/cli#v1.1.0 sign dist/myapp.exe
  artifacts:
    paths: [dist/myapp.exe]

Azure Pipelines

- script: npx -y github:bamboodeploy/cli#v1.1.0 sign $(Build.ArtifactStagingDirectory)/myapp.exe
  displayName: Sign Windows binary
  env:
    BAMBOO_API_KEY: $(BAMBOO_API_KEY)

CircleCI

- run:
    name: Sign Windows binary
    command: npx -y github:bamboodeploy/cli#v1.1.0 sign dist/myapp.exe

Jenkins

stage('Sign Windows binary') {
  environment { BAMBOO_API_KEY = credentials('bamboo-api-key') }
  steps { sh 'npx -y github:bamboodeploy/cli#v1.1.0 sign dist/myapp.exe' }
}

AppVeyor

after_build:
  - npx -y github:bamboodeploy/cli#v1.1.0 sign dist\myapp.exe

.NET / Visual Studio

A .NET global tool, no Node needed. Install once, then sign after publish. Works for WPF, WinForms, WinUI, console apps, and PublishSingleFile builds.

dotnet tool install -g BambooDeploy.Sign
bamboo-sign sign bin/Release/net8.0/publish/MyApp.exe

Add a post-publish target to the .csproj so every Release publish comes out signed:

<Target Name="BambooSign" AfterTargets="Publish" Condition="'$(Configuration)' == 'Release' And '$(BAMBOO_API_KEY)' != ''">
  <Exec Command="bamboo-sign sign "$(PublishDir)$(AssemblyName).exe"" />
</Target>

Or a Visual Studio post-build event: bamboo-sign sign "$(TargetPath)", on the Release configuration only. Same commands as the Node CLI: scan, status, delete, config, same exit codes. Package: BambooDeploy.Sign on NuGet.

C++ / CMake

add_custom_command(TARGET myapp POST_BUILD
  COMMAND npx -y github:bamboodeploy/cli#v1.1.0 sign $<TARGET_FILE:myapp>
  COMMENT "Signing with Bamboo Deploy")

Gate it on a cache option (if(BAMBOO_SIGN)) so local developer builds skip it. Same one-liner works from MSBuild post-build events, Makefiles, and Ninja.

Rust / Tauri / Go

Tauri (tauri.conf.json): run the CLI after bundling.

npm install --save-dev github:bamboodeploy/cli#v1.1.0
{ "bundle": { "windows": { "signCommand": "node node_modules/@bamboodeploy/cli/bin/bamboo.js sign %1" } } }

Tauri starts the sign command directly, without a shell, so it has to be a real executable like node. npx is a script on Windows and will not launch from here. Tauri calls the command once per binary (the app exe, then the installer), so each build creates two sign jobs.

Cargo: cargo build --release && npx github:bamboodeploy/cli sign target/release/myapp.exe, or add it to your release script.

GoReleaser (.goreleaser.yml):

builds:
  - id: windows
    goos: [windows]
    hooks:
      post: npx -y github:bamboodeploy/cli#v1.1.0 sign {{ .Path }}

Python / PyInstaller

PyInstaller output is the most common source of antivirus false positives we see. A pure standard library package, no Node needed. Scan first, then sign:

pip install bamboodeploy
pyinstaller --onefile app.py
bamboo-sign scan dist/app.exe --fail-on-findings
bamboo-sign sign dist/app.exe

python -m bamboodeploy works too, and the same calls are importable: from bamboodeploy import sign_file, scan_file. Same for Nuitka (--onefile) and cx_Freeze. Package: bamboodeploy on PyPI. See Why your PyInstaller EXE gets flagged as malware.

Inno Setup / Delphi

Inno Setup can call an external sign tool for the installer and every file in [Files] marked signonce. In the script:

[Setup]
SignTool=bamboo
SignedUninstaller=yes

Then in the Inno Setup IDE (Tools, Configure Sign Tools) register bamboo as:

cmd /c npx -y github:bamboodeploy/cli#v1.1.0 sign $f

The cmd /c prefix is needed because Inno Setup starts the tool directly and npx is a script on Windows.

Or from the command line: ISCC.exe "/Sbamboo=cmd /c npx -y github:bamboodeploy/cli#v1.1.0 sign $f" setup.iss. Delphi post-build event: npx -y github:bamboodeploy/cli#v1.1.0 sign "$(OUTPUTPATH)".

REST API

Base URL https://api.bamboodeploy.com. Auth with Authorization: Bearer bd_live_... (or X-API-Key). JSON responses. Send a real User-Agent header naming your tool: the default Python urllib agent is blocked at the edge with error 1010.

MethodPathWhat it does
GET/healthLiveness, no auth
POST/uploads?name=app.exeUpload. Body is the raw file (application/octet-stream, streamed, up to 95 MB; larger files use POST /uploads/init, PUT to the returned upload_url, then POST /uploads/:id/finalize, up to 650 MB. The CLI does this automatically) or multipart file= (up to 32 MB). Returns {id, status, duplicate, scan}. 201 new, 200 if the same bytes were already uploaded.
GET/uploadsList recent uploads with scan and sign status. ?limit= up to 100.
GET/uploads/:idApp metadata, scan summary (code_signing, packer, yara_matches, compiler, sha256), sign block (status, pending_reason, error)
GET/uploads/:id/reportFull scan report as text. 409 while the scan is still running.
POST/uploads/:id/signQueue signing. 202 {job_id, status}. 402 without Premium, 403 without verified connections, 422 for third-party vendor binaries.
GET/uploads/:id/download?variant=signedStream the signed file (variant=original for the upload). 404 until signing is done.
DELETE/uploads/:idDelete the app and its files, cancel pending sign jobs

Sign job status values: pending (waiting for scan or review), queued, signing, done, failed, denied, canceled. Poll every 10 s or so.

curl

AUTH="Authorization: Bearer $BAMBOO_API_KEY"
API=https://api.bamboodeploy.com

# 1. Upload (streamed)
ID=$(curl -sS -H "$AUTH" -H "Content-Type: application/octet-stream" \
  --data-binary @dist/myapp.exe "$API/uploads?name=myapp.exe" | jq -r .id)

# 2. Sign
curl -sS -X POST -H "$AUTH" "$API/uploads/$ID/sign"

# 3. Poll until .sign.status == "done"
curl -sS -H "$AUTH" "$API/uploads/$ID"

# 4. Download
curl -sS -H "$AUTH" -o dist/myapp.exe "$API/uploads/$ID/download?variant=signed"

Errors and exit codes

CLI exitAPIMeaning
02xxDone
14xx / 5xxBad key, file not found, sign denied or failed (message printed)
2402 premium_requiredSigning needs a Premium subscription
3403 connections_requiredVerify two publisher connections in the dashboard first
4n/aTimed out waiting. If pending_reason is operator_review, the job is waiting for a person; re-run after it is approved, or ask for Auto-sign.
5n/ascan --fail-on-findings found a packer or YARA match

Questions or a build system not listed here? Contact us and we will add it.