┌──────────────────────┐
│ action ─────┐ │
│ ▼ │
│ ╭─────────────╮ │
│ │ ▓ gate ▓ │ │
│ ╰─────────────╯ │
│ ╱ │ ╲ │
│ ✓ ? ✗ │
│ allow ask deny │
└──────────────────────┘
bareguard
One chokepoint between your agent and the world. Bounds what the agent does, not what it says. Single audit log. Hard caps that halt with a human in the loop. ~930 lines, one production dep.
bareguard is a runtime policy library every agent action passes through. One Gate class, three call sites (redact, check, record), twelve primitives — bash, fs, net, budget, content, secrets, audit, limits, tools, defer-rate, spawn-rate, approval. Each primitive is one small file you can read in a sitting.
Same patterns as bareagent, barebrowse, and baremobile — embed it, don't run it. No daemon, no SaaS, no telemetry.
Not a content guardrail (use guardrails-ai for toxicity / PII / schema). Not a sandbox (Docker / gVisor for containment). Not authn (caller's concern). Not a scheduler. The five-layer split: system prompt → guardrails-ai → bareguard → sandbox → OS perms. bareguard owns exactly one.
npm install bareguard
Requires Node.js >= 20. One production dep: proper-lockfile.
import { Gate } from "bareguard";
const gate = new Gate({
tools: { allowlist: ["bash", "read", "write", "fetch"] },
bash: { allow: ["git", "ls"], denyPatterns: [/sudo/, /rm\s+-rf/] },
fs: { writeScope: ["/tmp/agent"], readScope: ["/tmp"], deny: ["~/.ssh"] },
budget: { maxCostUsd: 5.00, maxTokens: 100_000 },
limits: { maxTurns: 50 },
humanChannel: async (event) => {
// event.kind: "ask" | "halt" — your UX decides (TUI, Slack, web, PIN)
return { decision: "allow" }; // or "deny" / "topup" / "terminate"
},
});
await gate.init();
// In your agent loop:
const decision = await gate.check(gate.redact(action));
if (decision.outcome === "allow") {
const result = await yourExecutor(action);
await gate.record(action, result); // result.costUsd / result.tokens
}
// gate.check never returns "askHuman" — bareguard resolves that internally
// via humanChannel and gives you a terminal allow/deny.Full integration guide for AI assistants and developers: bareguard.context.md — covers the humanChannel patterns (TUI / Slack / PIN), shared budget across processes, eval order, audit format, gotchas, and 8 recipes including the bareagent + beeperbox wiring.
Every action traverses one gate. The eval order is deny > ask > scope > default, first match wins:
tools.denylist→ denycontent.denyPatterns→ deny (universal — catchesDROP TABLE,rm -rf /on any tool)- per-action-type rules → deny (
bash/fs/net/limits.maxChildren/tools.denyArgPatterns) content.askPatterns→ ask the human (universal — fires even on allowlisted tools)tools.allowlistenforcement → allow if listed, deny if set+miss- default → allow
Pre-eval halt checks (budget, maxTurns, gate.terminated) run before step 1. Halt-severity events MUST escalate to a human via humanChannel; they NEVER bubble to the LLM.
One JSONL audit file per agent family. POSIX O_APPEND guarantees atomicity for writes < 4KB — same mechanism nginx access logs use. Parent + children + grandchildren all append the same file; grep parent_run_id reconstructs the tree. Windows uses a proper-lockfile fallback (auto-detected).
Every primitive is one file (~30–180 LOC).
| Primitive | What it does |
|---|---|
| bash | Command allowlist + denyPatterns when action.type === "bash". |
| budget | Tokens + cost USD, halt severity (escalates to human). Shared across processes via proper-lockfile. |
| fs | writeScope / readScope / deny for read / write / edit. Path prefix matching. |
| net | Egress domain allowlist + private-IP deny for fetch. |
| limits | maxTurns (halt), maxChildren (action), maxDepth (action), timeoutSeconds (halt, v0.2). |
| tools | Tool-name allowlist / denylist (glob-matched) + per-tool denyArgPatterns. Allowlist is scope-only — does not silence asks. |
| content | Pattern matches over serialized action. Universal denyPatterns (step 2) + askPatterns (step 4). Safe defaults shipped. |
| secrets | Redacts known env-var values + cred patterns. Tags with name ([REDACTED:ANTHROPIC_API_KEY]); never leaks. |
| audit | One JSONL file per family. Phases: gate, record, approval, halt, topup, terminate. |
| approval | Routes ask events to the runner-supplied humanChannel callback. |
| defer-rate | Caps defer actions per minute (default 15). Counted from the audit log; per-family. |
| spawn-rate | Caps spawn actions per minute (default 10). Composes with maxChildren / maxDepth. |
Safe defaults ship in content. rm -rf /, DROP TABLE, TRUNCATE denied outright. Destructive verbs (delete, revoke, force-push, destructive HTTP methods) escalate to the human. Override with empty arrays for pure-allow.
The design choices that surprise people most often. Read these before wiring it up.
1. tools.allowlist does NOT silence safe-default content.askPatterns. Allowlist is scope-only ("which tools can be invoked at all"), not a trust shortcut. To silence an ask: narrow content.askPatterns or use tools.denyArgPatterns.
2. Glob * matches anything including /. mcp:foo/admin_* catches mcp:foo/admin_baz AND mcp:foo/admin_baz/sub. Safe for denylists; can over-grant on allowlists — list specific tools or use a tighter prefix. v0.2 may add ** so * becomes "anything except /".
3. humanChannel is effectively required for safe-default-shipped configs. First time an ask fires without one wired, bareguard prints a one-time WARN to stderr and denies with severity: "halt". Headless / CI runs that intentionally have no channel see this once and continue.
4. Caps are soft, halts are hard. Cross-process budget can be exceeded by one action's spend before next refresh. Halt fires reliably on the next check after a record.
5. gate.check and gate.record MUST be called serially per Gate instance. Multiple Gate instances (parent + child processes) run independently and concurrently fine.
46 tests pass on the CI matrix: Linux + macOS + Windows × Node 20 + 22. Real subprocesses verify shared-budget contention under proper-lockfile, halt-cascade across processes, single-audit-file atomicity (3 concurrent writers, no torn lines), parent_run_id / spawn_depth stitching across a 3-deep tree, and maxChildren / maxDepth enforcement.
Four vanilla JS modules. Zero deps where possible (bareguard has one). Same API patterns.
| bareagent | barebrowse | baremobile | bareguard | |
|---|---|---|---|---|
| Does | Gives agents a think→act loop | Gives agents a real browser | Gives agents Android + iOS devices | Gates everything an agent does |
| How | Goal in → coordinated actions out | URL in → pruned snapshot out | Screen in → pruned snapshot out | Action in → allow / deny / human-asked out |
| Replaces | LangChain, CrewAI, AutoGen | Playwright, Selenium, Puppeteer | Appium, Espresso, XCUITest | Hand-rolled allowlists, scattered policy |
| Interfaces | Library · CLI · subprocess | Library · CLI · MCP | Library · CLI · MCP | Library |
| Solo or together | Orchestrates the others as tools | Works standalone | Works standalone | Embedded in bareagent's loop; usable by any runner |
Reach 50+ messengers with one Docker container via beeperbox — a headless Beeper Desktop that exposes WhatsApp, iMessage, Signal, Telegram, Slack, Discord, RCS, SMS and more as a single MCP server. Wire it through bareagent's MCP bridge; bareguard policies the invocations like any other tool.
- PRD — unified design spec.
- non-roadmap — the NO-GO list.
- decisions log — design calls resolved across versions.
- CHANGELOG — release-by-release diff.