Permissions
Heartbit’s permission system controls which tools an agent can execute and under what conditions. It combines declarative rules, human-in-the-loop approval, and persistent learned permissions.
Permission Rules
Section titled “Permission Rules”Rules are evaluated in order for each tool call — first match wins. Each rule specifies a tool name, an input pattern, and an action.
Actions
Section titled “Actions”| Action | Behavior |
|---|---|
allow | Execute without asking |
deny | Reject without asking (agent receives an error result) |
ask | Defer to the on_approval callback |
Rule Matching
Section titled “Rule Matching”- Tool name: exact match, or
"*"to match all tools - Pattern: glob matched against all string values in the tool’s JSON input.
"*"matches everything. Supports*(any characters) and?(single character).
If no rule matches, the tool call falls through to the on_approval callback (if set), or executes directly.
Configuration
Section titled “Configuration”TOML Config
Section titled “TOML Config”# Allow all read operations[[permissions]]tool = "read_file"action = "allow"
# Block destructive bash commands[[permissions]]tool = "bash"pattern = "rm *"action = "deny"
# Block access to .env files across all tools[[permissions]]tool = "*"pattern = "*.env*"action = "deny"
# Everything else: ask the human[[permissions]]tool = "*"action = "ask"Rules are evaluated top to bottom. Place specific rules before general ones.
Programmatic Setup
Section titled “Programmatic Setup”use heartbit::AgentRunner;use heartbit::agent::permission::{PermissionRuleset, PermissionRule, PermissionAction};
let rules = PermissionRuleset::new(vec![ PermissionRule { tool: "read_file".into(), pattern: "*".into(), action: PermissionAction::Allow, }, PermissionRule { tool: "bash".into(), pattern: "rm *".into(), action: PermissionAction::Deny, }, PermissionRule { tool: "*".into(), pattern: "*".into(), action: PermissionAction::Ask, },]);
let runner = AgentRunner::builder(provider) .name("worker") .system_prompt("You are a helpful assistant.") .permission_rules(rules) .build()?;Use PermissionRuleset::allow_all() to bypass all permission checks.
Human-in-the-Loop Approval
Section titled “Human-in-the-Loop Approval”The on_approval callback is invoked when a tool call has action Ask or when no permission rule matches (and the callback is set).
use heartbit::llm::ApprovalDecision;
let runner = AgentRunner::builder(provider) .name("worker") .system_prompt("You are a helpful assistant.") .on_approval(Arc::new(|tool_calls| { // Inspect tool_calls and decide ApprovalDecision::Allow })) .build()?;ApprovalDecision
Section titled “ApprovalDecision”| Decision | Effect |
|---|---|
Allow | Execute this time |
Deny | Reject this time |
AlwaysAllow | Execute and persist as a permission rule |
AlwaysDeny | Reject and persist as a permission rule |
When the user chooses AlwaysAllow or AlwaysDeny, the decision is injected into the live permission ruleset. Future matching tool calls skip the approval prompt.
CLI Usage
Section titled “CLI Usage”Enable interactive approval with the --approve flag:
heartbit run --config heartbit.toml --approve "Deploy the application"The CLI prompts before each tool execution, with options for Allow, Deny, AlwaysAllow, and AlwaysDeny.
Learned Permissions
Section titled “Learned Permissions”Persistent approval decisions are saved to a TOML file (default: ~/.config/heartbit/permissions.toml). These survive across sessions.
use heartbit::agent::permission::LearnedPermissions;
let learned = LearnedPermissions::load( &LearnedPermissions::default_path().unwrap())?;
let runner = AgentRunner::builder(provider) .name("worker") .system_prompt("You are a helpful assistant.") .learned_permissions(Arc::new(std::sync::Mutex::new(learned))) .build()?;How It Works
Section titled “How It Works”- User chooses
AlwaysAllowforbashtool - A rule
{ tool: "bash", pattern: "*", action: "allow" }is added to the live ruleset - The same rule is saved to
~/.config/heartbit/permissions.toml - On next session, the learned rules are loaded and appended to the ruleset
Config rules retain priority over learned rules (first match wins), since learned rules are appended after config rules.
Learned Permissions File Format
Section titled “Learned Permissions File Format”[[rules]]tool = "read_file"action = "allow"
[[rules]]tool = "bash"pattern = "rm *"action = "deny"Duplicate rules (same tool + pattern + action) are silently skipped.
Evaluation Order
Section titled “Evaluation Order”The full evaluation pipeline for each tool call:
Tool call | vPermission rules (first match wins) | +-- Allow --> Execute +-- Deny --> Error result to agent +-- Ask --> on_approval callback +-- No match --> on_approval callback (if set), else execute | +-- Allow/AlwaysAllow --> Execute +-- Deny/AlwaysDeny --> Error resultWhen AlwaysAllow or AlwaysDeny is returned, the decision is also persisted as a new rule in the live ruleset (and on disk if learned permissions are configured).
Events
Section titled “Events”Permission-related events are emitted through the observability system:
approval_requested— human approval prompt sent (includes tool names)approval_decision— human response received (approved or denied)guardrail_denied— if a guardrail (separate from permissions) blocks a tool
Common Patterns
Section titled “Common Patterns”Allow reads, gate writes
Section titled “Allow reads, gate writes”[[permissions]]tool = "read_file"action = "allow"
[[permissions]]tool = "grep"action = "allow"
[[permissions]]tool = "glob"action = "allow"
[[permissions]]tool = "*"action = "ask"Block sensitive files
Section titled “Block sensitive files”[[permissions]]tool = "*"pattern = "*.env*"action = "deny"
[[permissions]]tool = "*"pattern = "*secret*"action = "deny"
[[permissions]]tool = "*"pattern = "*password*"action = "deny"
[[permissions]]tool = "*"action = "allow"