Skip to content
PinataCloudPublic

About

Doorman — local-first monitor and guard for AI coding agents (desktop app)

Resources

Contributing

Stars

6 stars

Watchers

1 watching

Forks

Repository files navigation

Doorman

Doorman

Watches what your AI coding agents read and run, and flags the secrets they expose.
On your machine, with nothing sent anywhere.

Download · Model on Hugging Face · Built by Pinata

Security Radar, light theme Security Radar, dark theme

A local-first desktop app that watches the AI coding agents on this machine: what they spend in tokens, what they touch, and what they try to send. It runs on macOS (the primary target), with Linux (AppImage) and Windows (NSIS) builds from the same source. Built by Pinata.

Everything stays on the machine. Doorman reads the agents' own transcript files from disk and keeps its store under its own data directory. It has no account, no server and no telemetry. The one thing it downloads is described under Network.

Two ways to run it, sharing one data directory: the desktop app, or the command line for terminals, servers and scripts. See Get started.

The model. Doorman's on-device injection check runs Llama-Doorman-22M, a fine-tune of Llama Prompt Guard 2 (22M) for agent tool output, published on Hugging Face with its benchmark and file fingerprints. Built with Llama.

Rules you can read, packs you switch on, rules you write

Doorman's guard runs on rules you can read, change and write yourself.

Guardrails showing the Crypto wallets rule pack and four rules of your own, and in front of it a cast send to mainnet held by that pack, waiting for Deny or Allow

Three kinds of rules. Built-in rules: credentials, secrets, wallet keys and personal data, found by detectors that know the real formats; one security level sets them all. Rule packs: named sets of rules you switch on per machine, such as Crypto wallets, 12 rules for transfers, key exports and exchange APIs. Your own rules: patterns over the tool, the shell command, an MCP server or any argument; no code runs; dry run on your last 14 days first. What a rule does to a matching call, quieter to stricter: observe (record it, let it run), warn (let it run, raise a heads-up), ask (hold it until you answer), block (refuse it).

A rule of your own is a few lines of JSON:

{
  "id": "no-prod-apply",
  "label": "kubectl apply against prod",
  "severity": "high",
  "mode": "ask",
  "when": { "command": "kubectl\\s+apply\\b.*--context[= ]prod" }
}
doorman custom-rules dry-run rule.json   # what it would have caught
doorman custom-rules add rule.json       # hold it from the next call

How rules work → · Write your own → · Contribute a pack →

Screenshots use made-up sessions in a sandboxed profile.

Get started

Desktop app

Download the build for your machine from the latest release:

Machine File To install
Mac with Apple silicon Doorman-<version>-arm64.dmg Open it and drag Doorman into Applications. Signed and notarized, so it opens like any app.
Mac with Intel Doorman-<version>.dmg The same.
Windows 10 or 11 (x64 or Arm) Doorman-Setup-<version>.exe Run the installer. It is unsigned, so SmartScreen may say "Windows protected your PC": choose More info, then Run anyway. It installs for your user only, with no admin prompt.
Linux (x64) Doorman-<version>.AppImage chmod +x Doorman-*.AppImage, then run it. It needs FUSE 3 (fusermount3, from the fuse3 package; most desktops have it, a minimal install such as Arch may not), but not libfuse2. Without FUSE, run it with --appimage-extract-and-run. Running it adds Doorman, with its icon, to your app launcher, pointing at wherever the AppImage is (a later run from a new place moves the entry), so keep it somewhere permanent such as ~/Applications.
Linux (arm64) Doorman-<version>-arm64.AppImage The same.

On first launch, a short setup:

  1. Finds your AI tools. Doorman looks for the agents on this computer (Claude Code, Codex, Cursor and the others under What it does) and starts reading their transcripts, history included, so usage and findings fill in from what they have already done.
  2. Asks about the guard. Pause risky actions and ask me first is off until you turn it on, here or later in Guardrails → Protection. With it on, a risky tool call waits for your answer in a floating prompt before it runs. On Windows the guard needs Node.js on the PATH for now; on macOS and Linux it runs on the app's own runtime.
  3. Downloads the on-device model (about 96 MB) in the background, the one download described under Network.

The app checks for updates when it starts and every hour after, and installs them itself.

To remove it, turn Real time protection off first (Settings), which takes the guard's hook out of every agent, then delete the app. Its data stays in the data directory until you delete that too. On Linux, the launcher entry hides itself once the AppImage is gone; to remove it and its icons too:

rm -f "${XDG_DATA_HOME:-$HOME/.local/share}"/applications/cloud.pinata.doorman.desktop \
      "${XDG_DATA_HOME:-$HOME/.local/share}"/icons/hicolor/*/apps/cloud.pinata.doorman.png

Command line

The same Doorman, in a terminal and with no window, for servers, scripts and anyone who prefers it. It needs Node.js 22 or later.

npm install -g @pinata/doorman
doorman status      # where the data lives, what is guarded, what needs review
doorman scan        # read new agent transcripts and report what they exposed
doorman findings    # open exposures, grouped like the Security Radar

The guard works from the terminal too:

doorman guard install     # add the hook to every agent on this machine
doorman watch             # answer held tool calls here while the app is closed
doorman guard uninstall   # take the hook out again
  • doorman --help lists every command, and each one takes --help and --json.
  • Like the app, the first command run in a terminal that finds the model missing downloads it in the background (about 96 MB) and carries on (scripts and CI don't start it). Set DOORMAN_INJECTION_CLASSIFIER=off to skip the model.
  • It looks for a new version once a day and says so after a command's output; doorman update installs it and shows what changed.
  • Tab completion for zsh, bash and fish: doorman completion --help.
  • Without a global install: npx @pinata/doorman status.
  • With the desktop app on the same machine, both use the same store, policy and guard. While the app is running, the CLI reads a copy and asks you to make changes in the app; they can run at the same time, one after the other, or at different versions.
  • To remove it: doorman guard uninstall (if the app isn't installed), then npm uninstall -g @pinata/doorman.

The full command reference is in cli/README.md.

Docker Sandboxes (sbx)

The CLI also guards coding agents inside Docker Sandboxes: Claude Code, Codex, Copilot CLI, Cursor, Factory Droid, Gemini CLI and OpenCode (sbx agents claude, codex, copilot, cursor, droid, gemini, opencode). Use the kit in integrations/sbx-kit/: it installs the CLI, adds the guard before the agent starts, puts the hook back at every start (sbx rewrites the agent's settings on sbx kit add and in templates) and lets that sandbox reach Hugging Face for the model. sbx only accepts kits from sources you allow, so allow this one once (keep any sources you already allow in the list), then start your agent with it (Claude Code here; put another agent name in place of claude):

sbx settings set kit.allowedSources '["docker.io/","git+https://github.com/PinataCloud/doorman.git"]'
sbx run --name my-project claude . --kit "git+https://github.com/PinataCloud/doorman.git#dir=integrations/sbx-kit"
  • To answer held calls, run doorman sbx watch --all in a terminal on your computer (with the CLI installed there): one queue for every running sandbox, each card named after its sandbox. Without the CLI on your computer, sbx exec -it my-project doorman watch answers one sandbox (a closed terminal leaves that watch running; --replace takes over from it). sbx agents run without their own permission prompts, so allowing a call there runs it at once. With nobody watching, Claude Code, Copilot CLI, Cursor and Factory Droid ask in their own prompt; the others deny the call. doorman sbx ls shows each sandbox's Doorman, guard and open exposures.
  • Add it to a sandbox you already have: sbx kit add my-project <kit>. Don't install Doorman in a sandbox by hand: the next sbx kit add or template resets the agent's configuration and drops the hook without a word.
  • Pin the CLI version with --kit-arg doorman.version=0.6.0 (default: latest).
  • For v3 workloads (sbx run docker.io/docker/sbx-kit-claude:…), use the v3 kit: --kit "git+https://github.com/PinataCloud/doorman.git#dir=integrations/sbx-kit/v3/doorman". A v3 sandbox takes no sbx kit add, so give --kit when you create it.
  • Doorman's findings live inside the sandbox and sbx rm deletes them. doorman sbx pull my-project brings its sightings and sessions to the Doorman on your computer, where the app and doorman findings show them as "sandbox my-project"; --remove-after then removes the sandbox. The kit's README has an sbxenv.yaml that pulls on sbx env rm.
  • Only local sandboxes are supported. In a cloud sandbox (sbx --cloud) the doorman sbx commands see nothing, and answering held calls with sbx --cloud exec -it <name> doorman watch is untested.

What it does

  • Overview: the security landing page. It shows protection and the security level as a ring, the agents running right now (a held call counts down), the exposures still open to review, and recent sessions with something flagged.
  • Usage graphs: the last 7 or 30 days of input and output tokens per model, measured from the transcripts, and a weekday × hour punchcard of when you work with AI. Figures are token counts, never dollars: the vendored pricing table (main/lib/pricing.ts) still prices each turn internally, but an on-device estimate can't know how a turn was actually paid for.
  • Event Ledger: sessions and their turns, with local full-text search, deterministic titles (the agent's own title, else the first prompt), a per-turn strip that marks the turns with exposures, and the subscription-vs-overage split where the agent records it (Codex plan windows).
  • Security Radar: deterministic detectors (main/guard/security-detect.ts, with redaction first) run over every prompt, tool call and tool output, backfill included. Findings carry masked excerpts only and are grouped into exposures: one rule in one place (main/lib/exposures.ts), each reviewed with one click as Resolved, False positive or Accept risk. The Radar also has a Trace a secret view (renderer/screens/radar-trace.jsx) that draws where a value went.
  • Analytics: per-rule counters over a chosen window: what each rule found, what the guard stopped (blocked or denied), and which rules to set to Ask next.
  • Guardrails: real-time protection and how held calls wait, the security level (Relaxed, Watchful, Balanced — the default — or Locked down), each rule's mode with its recent activity, rule packs, your own rules (with a dry-run editor), exceptions and your wallet addresses. The families shown are narrowed by main/guard/rule-scope.ts. docs/rules.md explains rules, modes, packs and writing your own.
  • The guard: a PreToolUse hook that can watch, ask (hold the call until you answer in the app, a floating panel, a notification or VS Code), or block. Adapters live in main/guard/hosts/: Claude Code, Codex, Cursor, Gemini CLI, Copilot CLI, VS Code + Copilot, OpenCode, OpenClaw, Hermes Agent, Goose, Crush, Factory Droid, Kiro CLI, Kimi Code, Qwen Code and Grok Build.
  • MCP inventory and gateway: the MCP servers configured on the machine, tool-poisoning findings, a personal "recognized" list, and an optional stdio gateway that scans live tool descriptions (main/gateway/).
  • Doorman's own MCP server (main/mcp/doorman-mcp.ts): lets an agent ask the guard before acting, using five tools: doorman_check_tool_call, doorman_explain_last_block, doorman_session_spend, doorman_status and doorman_propose_rule. All five work from disk, so they work with the app closed; the last only saves a suggested rule for you to review (docs/rules.md).
  • VS Code companion (vscode-extension/): shows today's token usage and the guard's state in the status bar, and lets you answer held calls in the editor. It is packaged into a .vsix at build time and installed through the editor's own code CLI.
  • "Ask your agent": beside a finding, this builds a prompt from the masked evidence (main/ai-assistant.ts) for you to copy into your own coding agent. The app sends nothing itself.
  • Setup health (main/lib/doctor.ts): checks that the guard's hook entries, plugin files and bundle are in place, and that VS Code's settings let it run. It applies lossless repairs automatically and offers anything lossy as a click.
  • Appearance: Light, Dark or Auto (follows the OS) in Settings → Appearance. Both themes use the Pinata palette.

Usage, light theme Security Analytics, dark theme

Screenshots use made-up sessions in a sandboxed profile.

Network

  • Injection-classifier model (optional, on by default). When the classifier is on, the app downloads the model it needs into <userData>/models/, every file pinned by exact size and sha256 (main/lib/model-assets.ts), written only after it verifies, and re-hashed each time the app loads it (a file that no longer matches is re-fetched, with the other model running meanwhile if it is installed). Two models can be selected in Guardrails → Protection → AI-delivered protection:

    • Doorman (fine-tuned, recommended), the default: Llama Prompt Guard 2 22M fine-tuned for tool output (int8 ONNX, about 96 MB with its tokenizer). It downloads from Hugging Face, PinataCloud/Llama-Doorman-22M pinned to commit 4a43fec6 (DOORMAN_MODEL_BASE_URL in main/lib/model-sources.ts), or from DOORMAN_INJECTION_CLASSIFIER_SOURCE when that is set.
    • Prompt Guard 2 (stock): Llama Prompt Guard 2 22M (ONNX, about 82 MB) from Hugging Face (gravitee-io/Llama-Prompt-Guard-2-22M-onnx, pinned to one commit).

    While the fine-tuned model cannot be fetched (no network, an HTTP error, a file that fails its sha256 pin), the app downloads and runs the stock model in its place (never no model) and says so in Guardrails; it retries, and switches to the fine-tuned model by itself once that installs. So with the default selection the one download is the fine-tuned model from PinataCloud/Llama-Doorman-22M; stock is fetched only when it is chosen or the fine-tuned download fails. Both sets of weights are under the Llama 4 Community License, whose LICENSE and NOTICE files are downloaded and kept beside them. The ONNX Runtime WASM build that runs them ships inside the app (scripts/stage-runtime.mjs).

  • Second opinion (optional, off by default). This is a local model the guard can ask before a credential hold. Its endpoint must be loopback.

  • Updates. At start and hourly, electron-updater reads the latest release of the public PinataCloud/doorman repo on GitHub (the publish block in electron-builder.yml, baked into the app as app-update.yml). It sends no identifying data beyond a normal HTTPS request. A build without that file (--dir, a dev run) never checks.

What it writes outside its own directory

The app writes outside ~/Library/Application Support/Doorman/ only when you turn a feature on, and every write keeps a .bak beside the file:

  • The guard: a PreToolUse entry in each present agent's user-level hook document, for example ~/.claude/settings.json, ${CODEX_HOME:-~/.codex}/hooks.json, ~/.copilot/hooks/doorman.json and ~/.kiro/hooks/doorman.json. Plugin hosts also get a plugin directory (~/.openclaw/extensions/doorman-guard/, Hermes's plugins/doorman-guard/). The entry runs "<userData>/doorman/guard/node-runtime" "<userData>/doorman/guard/guard.js", so it survives app updates. node-runtime is a small launcher Doorman writes (main/lib/node-launcher.ts) that runs the guard on a Node.js runtime known to be on the machine: the CLI's, else the app's own (its executable with ELECTRON_RUN_AS_NODE; on Linux the AppImage file), and only then node from the PATH. So the guard runs on machines without Node.js, where agents such as Claude Code's native install and Codex don't need it — a hook that can't start is one every agent lets through. Doorman runs each recorded runtime once on a test script whenever it changes (recorded in runtimes.json) and leaves out any that fails; when none works, Settings, doorman doctor and doorman guard status say the guard can't start. Doorman's MCP server and the MCP gateway start the same way. (Windows still runs a bare node for now.)
  • The MCP server: an entry in each MCP host's config that points at <userData>/doorman/mcp/doorman-mcp.js (main/mcp-install.ts).
  • The VS Code extension (pinata.doorman): installed through code --install-extension.

One exception, on Linux only: the AppImage adds itself to your app launcher on every launch, because without a launcher entry GNOME shows its window with a generic icon, labelled cloud.pinata.doorman. It writes ~/.local/share/applications/cloud.pinata.doorman.desktop and ~/.local/share/icons/hicolor/<size>/apps/cloud.pinata.doorman.png (under $XDG_DATA_HOME when that is set), and keeps the entry pointing at wherever the AppImage last ran from. It never overwrites an entry of that name it didn't write, and writes nothing when AppImageLauncher or a similar tool has already integrated the AppImage. To remove it: delete those files.

Layout

  • main/: the Electron main process, TypeScript compiled to CommonJS in build/ (comments stripped by removeComments).
    • main/cli/: the doorman command line (cli/README.md). It runs this same main-process code under plain Node: index.ts dispatches, context.ts resolves the shared data directory (lib/data-dir.ts) and decides who may write (whoever holds the guard heartbeat owns the store; the CLI reads a copy meanwhile), commands/ holds one module per group. Nothing under main/cli/ may import a module that imports electron; scripts/build-cli-package.mjs fails the package if it does.
    • main/lib/: pure logic with no electron import, unit-tested.
    • main/guard/: the self-contained guard bundle, including guard/hosts/. It is compiled and copied to <userData>/doorman/guard/ at install time. Nothing in it may import from outside the directory, and every module must be listed in GUARD_FILES (main/enforcement.ts). A module the copy misses throws in someone's session, and the host reads that as an errored hook and fails open. policy.json, spool.jsonl, app.json (the heartbeat) and pending/ (held calls) live beside it.
  • renderer/: no bundler in development. It uses vendored React + Babel (assets/vendor/) over the private doorman:// scheme, with raw JSX compiled in-page and flat globals shared across scripts (loader order is SCREENS in renderer/index.html). Packaged builds ship a precompiled renderer: scripts/build-renderer.mjs concatenates the screens and runs one esbuild pass (JSX strip + minify) into build/renderer/. As a result the asar carries no raw source and no Babel, and the packaged CSP drops unsafe-eval. renderer/tokens.css holds the design tokens (fonts and palette).
  • assets/: fonts, vendored React/Babel and the brand mark. These are served to the renderer under _asset/ and shipped as Resources/assets.
  • vscode-extension/: the editor companion.
  • integrations/sbx-kit/: the Docker Sandboxes kit that adds the Doorman CLI and its guard to a sandbox running any of the agents it supports (sbx run <agent> --kit …), with its v3 variant in v3/doorman/; published by .github/workflows/sbx-kit.yml on kit-v* tags.
  • schemas/: committed JSON Schemas for the detection and ledger exports, generated from main/lib/schema-files.ts by npx vite-node scripts/make-detection-schema.ts. A test fails when they go stale.
  • test/: Vitest. test/fixtures/ holds recorded transcripts and hook payloads per agent.
  • docs/shipped-surface.md: what the packaged app contains, and what a downloader can read from it.
  • docs/rules.md: rules, modes, severity, levels, packs and your own rules, for the people using Doorman. CONTRIBUTING.md: setting up, and adding a rule pack or a built-in rule.

Data lives under ~/Library/Application Support/Doorman/ (%APPDATA%\Doorman on Windows, ~/.config/Doorman on Linux): store/ (the turn store, findings, titles, cursors and the Keychain-encrypted evidence vault), prefs.json, models/ (one directory per downloaded model revision, <name>@<pins>, each with a .verified.json recording the sha256 every file was checked against) and doorman/ (the guard and MCP bundles, and the locks the app and the CLI use to take turns writing, main/lib/data-owner.ts).

Develop

npm install
npm run dev              # compile + launch
DOORMAN_DEV=1 npm run dev   # + devtools, renderer console forwarded
npm run typecheck        # main + tests
npm test                 # vitest
npm run dev:sandbox      # throwaway home + profile, guard installable (see the script)
npm run compile && npm run probe   # drive the guard catalog through the shipped pipeline

Renderer edits need no compile: reload the window. Main-process edits need a fresh npm run dev.

npm run compile runs the icon build, brand (scripts/brand-dev-shell.mjs), runtime and vsix steps, then tsc. brand relabels the prebuilt node_modules/electron/dist/Electron.app, so that an unpackaged run shows Doorman with its mark instead of Electron. It sets the bundle name, the icon and a separate cloud.pinata.doorman.dev identifier, and it leaves the binary's name alone (the script explains why). Restart after the first branded compile.

DOORMAN_APPDATA_DIR=<dir> redirects the profile into a sandbox. A sandboxed run refuses to write hook documents in the real home.

Environment settings

The injection classifier has one row in Guardrails → Protection → AI-delivered protection: an on/off switch and a model dropdown (Doorman's fine-tuned model, the default, or stock Prompt Guard 2). The stored choice is written only when it is stock (injection_classifier.model in the policy); no value means the fine-tuned model, so an install from before the dropdown existed moves to it. The guard's credential second opinion has no Settings screen. Both can be set in the environment the app is launched from (main/lib/policy-env.ts); an environment value wins over Settings, and the control it pins shows read-only. Values are laid over the stored prefs through the same clamps, and an unset or unparseable value means "no opinion":

DOORMAN_INJECTION_CLASSIFIER=off                  # on | off (default on)
DOORMAN_INJECTION_CLASSIFIER_SUPPRESS_BELOW=0.35  # overrule threshold, clamped
DOORMAN_INJECTION_CLASSIFIER_MODEL=stock          # doorman-1 | stock (default doorman-1)
DOORMAN_INJECTION_CLASSIFIER_SOURCE=/path/to/package   # or https://host/prefix/; where doorman-1 comes from
DOORMAN_SECOND_OPINION=on                         # on | off (default off)
DOORMAN_SECOND_OPINION_ENDPOINT=http://127.0.0.1:11434/v1/chat/completions   # loopback only
DOORMAN_SECOND_OPINION_MODEL=qwen2.5:1.5b
DOORMAN_SECOND_OPINION_TIMEOUT_MS=3000

DOORMAN_INJECTION_CLASSIFIER_SOURCE overrides DOORMAN_MODEL_BASE_URL (main/lib/model-sources.ts) for the fine-tuned model: an http(s) prefix the file names are appended to, or a local directory holding the package. Either way every file is verified against the pins in main/lib/model-assets.ts.

Publishing a new revision of the fine-tuned model means setting that one constant, DOORMAN_MODEL_BASE_URL, to its pinned-revision Hugging Face URL (https://huggingface.co/<org>/<repo>/resolve/<commit>/), and updating the pins in main/lib/model-assets.ts only if the files changed. Installed apps fetch it at their next launch.

A Finder launch inherits nothing from a shell. For a packaged app, use launchctl setenv … and relaunch, or start it with open -a Doorman --env ….

Build

npm run build        # macOS: dmg + zip, arm64 and x64
npm run build:linux  # AppImage, x64 and arm64 (static AppImage runtime: needs FUSE 3, not libfuse2)
npm run build:win    # one NSIS installer covering x64 and arm64, per-user, unsigned

Output goes to dist/. Packaging is configured in electron-builder.yml. Its comments explain the files / extraResources split: the WASM runtime and the .vsix ship once, as real files under Resources/.

Signing (macOS). The hardened runtime is on, with the entitlements in build-resources/entitlements.mac.plist. scripts/adhoc-sign.mjs (the afterPack hook) re-signs the bundle ad-hoc unless a "Developer ID Application" identity is in the keychain. Without that step, arm64 builds would carry an invalid linker signature and macOS would report them as "damaged". Ad-hoc builds still need a one-time allow in System Settings → Privacy & Security. With a Developer ID available, electron-builder signs properly (CSC_LINK / CSC_KEY_PASSWORD, or the keychain), and it notarizes when APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER are set. Windows builds are unsigned.

Windows payloads. Build Windows installers with ELECTRON_BUILDER_7Z_FILTER=BCJ set, as the release workflow does. Without it, electron-builder's 7-Zip packs the arm64 executables with its ARM64 filter, which the installer can't decode: it reports success and installs no Doorman.exe on Windows on ARM (0.4.0 shipped that way). Check an installer with node scripts/verify-win-installer.mjs dist/Doorman-Setup-<version>.exe (needs 7-Zip on PATH).

Releasing

.github/workflows/release.yml runs on a pushed v* tag: tests, then the Mac (dmg + zip, x64 + arm64), Linux (AppImage) and Windows (NSIS) builds in parallel, each launched once as a smoke test, then publishes every file to a release on this repo, which is where installed apps look for updates.

# write the "## App 0.2.0 — <date>" section in CHANGELOG.md (from ## Unreleased)
npm version 0.2.0 --no-git-tag-version   # bumps package.json + lock
git commit -am "chore: release 0.2.0"
git tag v0.2.0 && git push origin main v0.2.0

Both release workflows publish that version's CHANGELOG.md section as the release notes (scripts/release-notes.mjs) and stop before building when it is missing.

The tag must match package.json or the run stops. Publishing uses the run's own GITHUB_TOKEN; the only secrets the workflow reads are the optional Apple signing and notary ones listed at the top of the file. Unsigned Mac builds install with a one-time "Open Anyway", but macOS will not apply an auto-update to them, so Mac auto-update needs the Developer ID.

Those secrets are set (2026-10-14): Mac releases are signed with the Developer ID Application: Pinata Technologies Inc (GUFNBHK267) identity and notarized through that team's App Store Connect API key (W5X5576RG4). v0.1.0 predates them and is ad-hoc signed, so its installs cannot auto-update and need one manual download of a signed release.

The CLI (@pinata/doorman on npm) has its own version in cli/package.json and its own workflow, .github/workflows/cli-release.yml, run by a cli-v* tag: tests, npm run cli:package (which assembles dist/cli-package and checks the bin runs on its own), an install of the packed tarball into an empty project, npm publish (needs the NPM_TOKEN secret), and a GitHub release holding the tarball. That release is never marked latest, because the desktop app's updater reads the latest release.

# bump "version" in cli/package.json, write its "## CLI <x.y.z> — <date>"
# section in CHANGELOG.md, commit, then:
git tag cli-v0.1.0 && git push origin cli-v0.1.0

Brand assets

Every brand asset derives from one master, assets/doorman-logo.png. scripts/make-brand.py (Pillow + fontTools, needed only to regenerate; the outputs are committed) writes:

  • assets/doorman-mark-512.png
  • assets/doorman-app-icon.png, the mark on a white tile (macOS before 26, and Linux)
  • assets/doorman-app-icon-win.png, the mark ringed in white with no tile (Windows, which draws app icons as shapes)
  • assets/mac/Doorman.icon, the Icon Composer document for macOS 26 and later, with a Dark variant
  • the menu-bar silhouette mask, main/lib/tray-mask.ts
  • the VS Code status-bar glyph font, vscode-extension/doorman-icons.woff

scripts/make-icons.mjs then slices the .icns, the Linux PNG and the Windows PNG on every build. npm run icons:mac (scripts/make-mac-icon.mjs) compiles Doorman.icon into assets/mac/Assets.car. It needs Xcode 26 or later, so it is run by hand after the mark changes and its output is committed: the release runner's Xcode is older.

Fonts. Hanken Grotesk ships under the SIL OFL (assets/fonts/HankenGrotesk-OFL.txt) and is the only bundled typeface. Monospace text uses the platform's own monospace font.

License

Doorman is licensed under the Apache License, Version 2.0. Copyright 2026 Pinata Technologies Inc. See NOTICE for attribution and the third-party code and assets in this repository.

The licence covers the code, not the "Doorman" or "Pinata" names and logos. The model weights the app downloads (Llama-Doorman-22M and stock Llama Prompt Guard 2) are not part of this repository and are licensed separately under the Llama 4 Community License. Built with Llama.

About

Doorman — local-first monitor and guard for AI coding agents (desktop app)

Resources

Contributing

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages