Local Android signing daemon and Nostr signer. It never runs Gradle. Read SECURITY.md before using a production key.
Use Hotbox if you want agents or local tools to sign APKs and Nostr events
without holding the vault password, JKS, or nsec. You unlock it, agents get
only a Unix socket path. Agents should follow SKILL.md. Ideal for
publishing Android apps to Zapstore.
Skip it if you need CI signing, multi-user access, or a Gradle plugin.
go build -trimpath -o hotbox ./cmd/hotbox
mkdir -m 700 -p "$HOME/.hotbox"
cd /path/to/android/project # becomes the allowed APK workspace
./hotbox my-appFirst run prompts for a Hotbox password (12+ characters), an Android alias, an optional existing keystore, and an optional Nostr private key. Blank keystore or Nostr key means Hotbox creates them. An imported JKS must already use the Hotbox password for store and keys.
Hotbox prints a socket path once. Give agents only that path:
export HOTBOX_SOCKET=/tmp/hb-UID-random/s.sock
curl --silent --show-error --fail-with-body \
--unix-socket "$HOTBOX_SOCKET" \
http://localhost/v1/statusOne encrypted vault holds one shared Android JKS, its public alias list, and one Nostr identity. Create one alias per Android app. Start and unlock Hotbox yourself; it then accepts local Unix-socket requests from programs running as your OS user until it stops.
An agent receives only the random Unix socket path for one unlocked session. Through it, the agent can sign an already-built APK, link its Android identity, sign one Nostr event, or issue a bounded NIP-46 session. It never needs a Hotbox executable, vault path, password, JKS, or Nostr private key.
APK identity linking publishes a Nostr proof to configured relays. Other operations make outbound connections only through explicitly created NIP-46 sessions.
The vault must sit outside the APK workspace. With no path argument, Hotbox
asks for a .hotbox filename and stores it in ~/.hotbox. A bare name such as
test first uses an existing ~/.hotbox/test.hotbox; otherwise it is treated
as a relative path.
The Hotbox password encrypts the vault and protects the JKS. New Android
certificates use the vault filename as their subject, for example
developer.hotbox creates CN=developer. Leave the keystore path blank to
create a 4096-bit RSA JKS with keytool.
An imported keystore remains at its original path. Move that file to offline backup or remove it after checking that the new vault opens correctly.
Stop Hotbox, then add a signing identity for another Android app without creating another vault:
./hotbox add-alias "$HOME/.hotbox/developer.hotbox" my-other-appSupply the vault and a new destination file. Hotbox prompts for the vault password again and refuses to replace an existing destination.
./hotbox export "$HOME/.hotbox/my-app.hotbox" "$HOME/release.jks"The exported JKS and its .json metadata sidecar are mode 0600. The sidecar
contains aliases and the Nostr npub, never a password or nsec.
Each unlock creates a random mode-0600 socket inside a new mode-0700 directory
under /tmp. That path is full signing authority for the daemon session.
Discover public status:
curl --silent --show-error --fail-with-body \
--unix-socket "$HOTBOX_SOCKET" \
http://localhost/v1/status{
"ok": true,
"bunker": true,
"identity_linking": true,
"aliases": ["my-app"],
"certificates": [
{
"name": "my-app",
"certificate_sha256": "AB:CD:...",
"certificate_dn": "CN=my-app"
}
],
"npub": "npub1...",
"workspace": "/absolute/project/workspace"
}npub is omitted when Nostr signing is unavailable. The response never
contains private material.
Sign an APK with a workspace-relative path:
curl --unix-socket "$HOTBOX_SOCKET" \
--fail-with-body \
-X POST http://localhost/v1/apk/sign \
-H 'Content-Type: application/json' \
--data '{"apk":"app/build/outputs/apk/release/app-release-unsigned.apk","output":"app/build/outputs/apk/release/app-release.apk","alias":"my-app"}'Hotbox copies the input to private scratch storage, signs the copy, verifies the certificate, and publishes the output without replacing an existing file.
{
"ok": true,
"output": "app/build/outputs/apk/release/app-release.apk",
"certificate_sha256": "AB:CD:..."
}The signed path is output, not apk. Both apk and the optional output
must be workspace-relative, and output must be beside apk. If output is
omitted, Hotbox uses app-release-unsigned-signed.apk. Add
"overwrite":true to replace an existing output; otherwise the request fails
with an actionable error and hint.
Link the Android certificate to the Nostr identity:
curl --unix-socket "$HOTBOX_SOCKET" --fail-with-body \
-X POST http://localhost/v1/apk/identity \
-H 'Content-Type: application/json' \
--data '{"alias":"my-app"}'Hotbox keeps the JKS and password inside the daemon while generating and
publishing the kind 30509 proof.
POST /v1/nostr/sign signs one unsigned event JSON object and returns the
complete event. It accepts every event kind. Useful when nak or another tool
will only publish the result.
POST /v1/nostr/sessions creates a bounded NIP-46 session for ngit, nak, zsp,
or another compatible client:
{"ttl":"15m","uses":64}ttl defaults to "15m" and may be no more than one hour. uses defaults to
64 and must be from 1 through 1024.
{
"id": "session-id",
"url": "bunker://...?",
"expires_at": "2026-08-06T13:35:00Z",
"uses_remaining": 64
}The url capability is returned only once. It binds to the first client,
allows signing any event kind, tracks successful signatures, and expires after
at most one hour. Do not print or persist url; retain only id to revoke the
session. GET /v1/nostr/sessions lists redacted sessions. Revoke one with
DELETE /v1/nostr/sessions/{id} when the client exits.
Sessions expose only get_public_key, ping, and sign_event; encryption,
decryption, and private-key export are denied. Current ngit and zsp clients use
a short-lived loopback WebSocket relay created only for that session.
With a running Hotbox daemon and Android build tools available:
HOTBOX_SOCKET=/tmp/hb-UID-random/s.sock \
HOTBOX_ALIAS=my-app ./scripts/smoke-sign-apk