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
- Open the dashboard, go to API Keys, create a key. It starts with
bd_live_and is shown once. - Export it as
BAMBOO_API_KEY, or pass--api-key, or putBAMBOO_API_KEY=bd_live_...in a.bamboorcfile in the project or home directory. - 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.
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
| Flag | Meaning |
|---|---|
--out | Write the signed file here instead of replacing the input |
--timeout | Seconds to wait for signing, default 900 |
--json | Machine-readable output on stdout, progress stays on stderr |
--fail-on-findings | scan exits 5 when a packer or YARA match is reported |
--api-key, --api-base | Override 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.
| Method | Path | What it does |
|---|---|---|
| GET | /health | Liveness, no auth |
| POST | /uploads?name=app.exe | Upload. 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 | /uploads | List recent uploads with scan and sign status. ?limit= up to 100. |
| GET | /uploads/:id | App metadata, scan summary (code_signing, packer, yara_matches, compiler, sha256), sign block (status, pending_reason, error) |
| GET | /uploads/:id/report | Full scan report as text. 409 while the scan is still running. |
| POST | /uploads/:id/sign | Queue signing. 202 {job_id, status}. 402 without Premium, 403 without verified connections, 422 for third-party vendor binaries. |
| GET | /uploads/:id/download?variant=signed | Stream the signed file (variant=original for the upload). 404 until signing is done. |
| DELETE | /uploads/:id | Delete 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 exit | API | Meaning |
|---|---|---|
| 0 | 2xx | Done |
| 1 | 4xx / 5xx | Bad key, file not found, sign denied or failed (message printed) |
| 2 | 402 premium_required | Signing needs a Premium subscription |
| 3 | 403 connections_required | Verify two publisher connections in the dashboard first |
| 4 | n/a | Timed 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. |
| 5 | n/a | scan --fail-on-findings found a packer or YARA match |
Questions or a build system not listed here? Contact us and we will add it.