Adom Bridge (macOS)
Public Made by Adomby adom
Adom Bridge for macOS: the menu-bar daemon that connects AI tools to your Mac.
name: adom-desktop-permissions description: > How the Adom Bridge human-approval permission gate works for an AI caller. Read this when a verb refuses with errorCode needs_approval or reason_required, when you need shell approval / bypass permissions on a machine, when you must call request_shell_approval to surface the human approval dialog, when a call demands a reason arg, or before an unattended run that touches shell_execute, run_script, write_file, or any other gated verb. Covers the human approval gate, the Approve 1 hr / 24 hr / Bypass Permissions buttons, why grants are human-only, and the macOS TCC permissions (Screen Recording, Accessibility) that sit underneath Bridge's own gate.
Adom Bridge permissions: the human approval gate for AI callers
The trust model in one paragraph
Bridge binds 127.0.0.1 only, and installing Bridge (or a bridge plugin) is
the user's consent to be driven, so every LOCAL caller (the CLI on the same
box, Adom Hydrogen, and bridges on the loopback direct API) is trusted by
transport and never gated. The approval gate applies to exactly one origin:
the remote relay / cloud-AI path (via_relay), a thread driving the machine
from off-box that the user may not be watching. If you are calling over the
relay, the gated verbs below require a human grant. Read-only verbs
(screenshots, status, window and UI reads, discovery) are never gated.
Which verbs gate
The gated class is "runs code / writes / deletes / kills processes / synthesizes input":
- Arbitrary code execution:
shell_execute,run_script(bash / zsh / sh),launch_process,wiki_exec - Filesystem:
write_file,delete_file - Process control:
process_kill - Synthetic input (drive the machine as the user):
desktop_click/desktop_type/desktop_press_keyand the Accessibility (AX) action verbs; these ride the SAME grant window as shell
One human grant covers the whole class. A bridge's own exec verbs
(browser_evaluate, nbrowser_eval, blender_run_python,
fusion_run_modeling_script, ...) gate per the bridge's declared risk
field, independently of this class; check that bridge's describe output, not
this doc.
The reason requirement: every gated call must explain itself
Every permission-class verb sent over the relay MUST carry a reason arg:
one plain sentence saying WHY you need this. The human sees it verbatim in
the approval prompt, attributed as "The AI says: ..." (omitting it shows
"(no reason given)", which a cautious user should decline). It also lands in
Bridge's activity log.
adom-desktop shell_execute '{"command":"brew install kicad",
"reason":"Installing KiCad so I can export your gerbers"}'
Write the reason for the human, not for the log: say what you are trying to accomplish, not what the command does. Good: "Installing the KiCad CLI so I can export your gerbers". Weak: "running npm install" (that is just the command again). The reason is capped at 300 chars and never replaces the command preview; the human always sees both.
A permission-class call with NO reason is refused before any approval path:
{
"success": false,
"errorCode": "reason_required",
"error": "'write_file' needs human permission, so it REQUIRES a `reason`. Retry the SAME call with reason:\"...\".",
"retryWith": { "reason": "<why you need this, in one plain sentence>" },
"appliesTo": ["shell_execute", "run_script", "launch_process", "wiki_exec", "write_file", "delete_file", "process_kill"]
}
The rule is class-based and flat: the verbs in appliesTo ALWAYS carry a
reason (whether or not a grant is currently active); verbs not in that list
never need one. Learn it once: do not pay the round-trip again, and do not
add reason to ungated verbs.
What a refusal looks like
When no grant is active, a gated relay call refuses with
errorCode: "needs_approval" plus a structured approval block; you never
need to read ~/.adom/config.json:
{
"success": false,
"errorCode": "needs_approval",
"error": "'write_file' needs shell approval - it runs code / writes / deletes / kills a process (a Tier-2 verb, as dangerous as shell_execute).",
"approval": {
"required": true,
"granted": false,
"class": "shell",
"verb": "write_file",
"grantedBy": "human-only",
"grantVerb": "request_shell_approval",
"howToGrant": ["Ask the user to click Approve (1 hr / 24 hr) or Bypass Permissions ...",
"Call request_shell_approval to re-surface the prompt ...",
"The user can tick Settings > Shell Commands > Bypass Permissions ..."]
}
}
The refusal itself already surfaced a human-facing prompt on the machine: the
Bridge window SUMMONS ITSELF to show the approval dialog (Bridge is normally a
menu-bar agent with no window; an approval flips it from Accessory to Regular,
shows the window, and activates it), and a Notification Center notification
fires as well. If the human actively clicks Deny (embedded mode), you get
errorCode: "denied_by_user" instead; do NOT auto-retry that one, ask the
user what they want.
The correct AI flow on a gate hit
Surface the prompt (the gate already did once; re-surface on demand):
adom-desktop request_shell_approvalThis is ungated; it only SURFACES, never grants. It re-shows the approval dialog and brings the Bridge window to the front, so "there is nothing to click" is never true.
Ask the human (in your conversation) to click Approve 1 hr, Approve 24 hr, or Bypass Permissions in the Adom Bridge window.
RETRY the original verb unchanged (with its
reason). One grant covers the whole exec/write/kill/input class for the granted window.
Grants are HUMAN-ONLY: you cannot self-grant
Granting is permanently human-only. The only grant paths are a human clicking
in Bridge: the approval dialog's Approve buttons and the Settings checkbox. A
GRANT attempt via shell_auto_approve / set_shell_auto_approve from a relay
(cloud-AI) caller is refused with errorCode: "human_only" and auto-surfaces
the human prompt instead. Your only lever is request_shell_approval; it
TRIGGERS the human decision, it does not perform it.
The one exception: a REVOKE is allowed for any caller (de-escalation is always safe):
adom-desktop shell_auto_approve '{"duration_secs":0}' # clears the checkbox AND the timer
Do not look for a workaround. launch_process is not a bypass of
shell_execute (it gates identically), and synthetic input (clicking the
Approve button yourself) rides the same grant. If the AI could grant, the
gate would be theater.
The durable Settings checkbox
Settings > Shell Commands > Bypass Permissions (config
shell_auto_approve_permanent, default OFF) is the durable indefinite grant,
for machines the user fully trusts. The timed buttons (1 hr / 24 hr) set an
in-memory window instead. Unticking the checkbox, or
shell_auto_approve {"duration_secs":0}, returns to per-command approval
instantly.
Embedded mode: Adom Hydrogen owns the decision
When Bridge runs embedded under Adom Hydrogen (Hydrogen passed auto_approve
in POST /embedded/enter), the human's app is Hydrogen and Bridge shows no UI
of its own. Each gated relay command is FORWARDED to Hydrogen's
/permission/request; Hydrogen renders the prompt (with your reason and a
decoded command preview) and returns the decision:
allow_once: run this one command, no windowallow_1h/allow_24h: set a timed grant window and runbypass_while_running: session-length grant (clears on restart)deny: you geterrorCode: "denied_by_user"; do not auto-retry
While forwarding is active, a standing grant (flag / checkbox / timer) does
NOT suppress the forward; Hydrogen decides every time. If Hydrogen is
unreachable, Bridge falls back to its own dialog. auto_approve: true means
the user opted in to auto-approval.
macOS TCC permissions (underneath Bridge's gate)
Bridge's approval gate governs WHO may drive the machine; macOS's own TCC privacy system separately governs what the Bridge PROCESS may do, and no Bridge grant can override it. In System Settings > Privacy & Security the app appears as Adom Bridge:
- Screen Recording: required for window and screen screenshots. Without it, captures come back blank or fail.
- Accessibility: required for synthetic input (CGEvent mouse/keyboard)
and the AX read/action verbs. Without it, gated input verbs fail with a
clear error naming the missing grant (macOS silently ignores CGEvent
posting otherwise, so Bridge checks
AXIsProcessTrustedfirst and refuses loudly).
If a verb says the Accessibility or Screen Recording permission is missing,
that is a one-time human toggle in System Settings, not something
request_shell_approval can grant.
Check state up front with status
Before an unattended run, read status.permissions.shell so you know whether
you will stall on a human gate:
adom-desktop status
"permissions": {
"shell": {
"autoApproved": false,
"willPrompt": true,
"source": "none", // "permanent" | "timed" | "none"
"expiresInSec": null,
"grantedBy": "human-only",
"grantVerb": "request_shell_approval",
"gatedFor": "relay (cloud-AI) callers only",
"embeddedPolicy": "standalone" // "forward-to-hd" | "auto-approve" | "standalone"
},
"bridgeExec": { "note": "bridge exec verbs gate per the bridge's declared risk field" }
}
If willPrompt is true and no human is at the machine, get the grant FIRST
(request_shell_approval plus asking the user) before starting the run.
Quick reference
| Situation | What you do |
|---|---|
errorCode: reason_required |
Resend the SAME call plus reason:"<one plain sentence>" |
errorCode: needs_approval |
request_shell_approval, ask the human to click Approve, retry |
errorCode: human_only |
You tried to self-grant. Ask the human instead |
errorCode: denied_by_user |
The human said no. Do not auto-retry; ask them |
| A capture/input verb reports a missing macOS permission | Ask the user to enable Adom Bridge under System Settings > Privacy & Security (Screen Recording or Accessibility) |
| Planning an unattended run | Check status.permissions.shell.willPrompt first |
| Need to drop a grant | shell_auto_approve {"duration_secs":0} (allowed for any caller) |
---
name: adom-desktop-permissions
description: >
How the Adom Bridge human-approval permission gate works for an AI caller.
Read this when a verb refuses with errorCode needs_approval or reason_required,
when you need shell approval / bypass permissions on a machine, when you must
call request_shell_approval to surface the human approval dialog, when a call
demands a reason arg, or before an unattended run that touches shell_execute,
run_script, write_file, or any other gated verb. Covers the human approval
gate, the Approve 1 hr / 24 hr / Bypass Permissions buttons, why grants are
human-only, and the macOS TCC permissions (Screen Recording, Accessibility)
that sit underneath Bridge's own gate.
---
# Adom Bridge permissions: the human approval gate for AI callers
## The trust model in one paragraph
Bridge binds `127.0.0.1` only, and installing Bridge (or a bridge plugin) is
the user's consent to be driven, so every LOCAL caller (the CLI on the same
box, Adom Hydrogen, and bridges on the loopback direct API) is trusted by
transport and never gated. The approval gate applies to exactly one origin:
the remote relay / cloud-AI path (`via_relay`), a thread driving the machine
from off-box that the user may not be watching. If you are calling over the
relay, the gated verbs below require a human grant. Read-only verbs
(screenshots, status, window and UI reads, discovery) are never gated.
## Which verbs gate
The gated class is "runs code / writes / deletes / kills processes /
synthesizes input":
- **Arbitrary code execution:** `shell_execute`, `run_script` (bash / zsh /
sh), `launch_process`, `wiki_exec`
- **Filesystem:** `write_file`, `delete_file`
- **Process control:** `process_kill`
- **Synthetic input** (drive the machine as the user): `desktop_click` /
`desktop_type` / `desktop_press_key` and the Accessibility (AX) action
verbs; these ride the SAME grant window as shell
One human grant covers the whole class. A bridge's own exec verbs
(`browser_evaluate`, `nbrowser_eval`, `blender_run_python`,
`fusion_run_modeling_script`, ...) gate per the bridge's declared `risk`
field, independently of this class; check that bridge's describe output, not
this doc.
## The `reason` requirement: every gated call must explain itself
Every permission-class verb sent over the relay MUST carry a `reason` arg:
one plain sentence saying WHY you need this. The human sees it verbatim in
the approval prompt, attributed as "The AI says: ..." (omitting it shows
"(no reason given)", which a cautious user should decline). It also lands in
Bridge's activity log.
```bash
adom-desktop shell_execute '{"command":"brew install kicad",
"reason":"Installing KiCad so I can export your gerbers"}'
```
Write the reason for the human, not for the log: say what you are trying to
accomplish, not what the command does. Good: "Installing the KiCad CLI so I
can export your gerbers". Weak: "running npm install" (that is just the
command again). The reason is capped at 300 chars and never replaces the
command preview; the human always sees both.
A permission-class call with NO reason is refused before any approval path:
```json
{
"success": false,
"errorCode": "reason_required",
"error": "'write_file' needs human permission, so it REQUIRES a `reason`. Retry the SAME call with reason:\"...\".",
"retryWith": { "reason": "<why you need this, in one plain sentence>" },
"appliesTo": ["shell_execute", "run_script", "launch_process", "wiki_exec", "write_file", "delete_file", "process_kill"]
}
```
The rule is class-based and flat: the verbs in `appliesTo` ALWAYS carry a
reason (whether or not a grant is currently active); verbs not in that list
never need one. Learn it once: do not pay the round-trip again, and do not
add `reason` to ungated verbs.
## What a refusal looks like
When no grant is active, a gated relay call refuses with
`errorCode: "needs_approval"` plus a structured `approval` block; you never
need to read `~/.adom/config.json`:
```json
{
"success": false,
"errorCode": "needs_approval",
"error": "'write_file' needs shell approval - it runs code / writes / deletes / kills a process (a Tier-2 verb, as dangerous as shell_execute).",
"approval": {
"required": true,
"granted": false,
"class": "shell",
"verb": "write_file",
"grantedBy": "human-only",
"grantVerb": "request_shell_approval",
"howToGrant": ["Ask the user to click Approve (1 hr / 24 hr) or Bypass Permissions ...",
"Call request_shell_approval to re-surface the prompt ...",
"The user can tick Settings > Shell Commands > Bypass Permissions ..."]
}
}
```
The refusal itself already surfaced a human-facing prompt on the machine: the
Bridge window SUMMONS ITSELF to show the approval dialog (Bridge is normally a
menu-bar agent with no window; an approval flips it from Accessory to Regular,
shows the window, and activates it), and a Notification Center notification
fires as well. If the human actively clicks Deny (embedded mode), you get
`errorCode: "denied_by_user"` instead; do NOT auto-retry that one, ask the
user what they want.
## The correct AI flow on a gate hit
1. **Surface the prompt** (the gate already did once; re-surface on demand):
```bash
adom-desktop request_shell_approval
```
This is ungated; it only SURFACES, never grants. It re-shows the approval
dialog and brings the Bridge window to the front, so "there is nothing to
click" is never true.
2. **Ask the human** (in your conversation) to click **Approve 1 hr**,
**Approve 24 hr**, or **Bypass Permissions** in the Adom Bridge window.
3. **RETRY the original verb unchanged** (with its `reason`). One grant
covers the whole exec/write/kill/input class for the granted window.
## Grants are HUMAN-ONLY: you cannot self-grant
Granting is permanently human-only. The only grant paths are a human clicking
in Bridge: the approval dialog's Approve buttons and the Settings checkbox. A
GRANT attempt via `shell_auto_approve` / `set_shell_auto_approve` from a relay
(cloud-AI) caller is refused with `errorCode: "human_only"` and auto-surfaces
the human prompt instead. Your only lever is `request_shell_approval`; it
TRIGGERS the human decision, it does not perform it.
The one exception: a REVOKE is allowed for any caller (de-escalation is
always safe):
```bash
adom-desktop shell_auto_approve '{"duration_secs":0}' # clears the checkbox AND the timer
```
Do not look for a workaround. `launch_process` is not a bypass of
`shell_execute` (it gates identically), and synthetic input (clicking the
Approve button yourself) rides the same grant. If the AI could grant, the
gate would be theater.
## The durable Settings checkbox
**Settings > Shell Commands > Bypass Permissions** (config
`shell_auto_approve_permanent`, default OFF) is the durable indefinite grant,
for machines the user fully trusts. The timed buttons (1 hr / 24 hr) set an
in-memory window instead. Unticking the checkbox, or
`shell_auto_approve {"duration_secs":0}`, returns to per-command approval
instantly.
## Embedded mode: Adom Hydrogen owns the decision
When Bridge runs embedded under Adom Hydrogen (Hydrogen passed `auto_approve`
in `POST /embedded/enter`), the human's app is Hydrogen and Bridge shows no UI
of its own. Each gated relay command is FORWARDED to Hydrogen's
`/permission/request`; Hydrogen renders the prompt (with your `reason` and a
decoded command preview) and returns the decision:
- `allow_once`: run this one command, no window
- `allow_1h` / `allow_24h`: set a timed grant window and run
- `bypass_while_running`: session-length grant (clears on restart)
- `deny`: you get `errorCode: "denied_by_user"`; do not auto-retry
While forwarding is active, a standing grant (flag / checkbox / timer) does
NOT suppress the forward; Hydrogen decides every time. If Hydrogen is
unreachable, Bridge falls back to its own dialog. `auto_approve: true` means
the user opted in to auto-approval.
## macOS TCC permissions (underneath Bridge's gate)
Bridge's approval gate governs WHO may drive the machine; macOS's own TCC
privacy system separately governs what the Bridge PROCESS may do, and no
Bridge grant can override it. In System Settings > Privacy & Security the app
appears as **Adom Bridge**:
- **Screen Recording**: required for window and screen screenshots. Without
it, captures come back blank or fail.
- **Accessibility**: required for synthetic input (CGEvent mouse/keyboard)
and the AX read/action verbs. Without it, gated input verbs fail with a
clear error naming the missing grant (macOS silently ignores CGEvent
posting otherwise, so Bridge checks `AXIsProcessTrusted` first and refuses
loudly).
If a verb says the Accessibility or Screen Recording permission is missing,
that is a one-time human toggle in System Settings, not something
`request_shell_approval` can grant.
## Check state up front with `status`
Before an unattended run, read `status.permissions.shell` so you know whether
you will stall on a human gate:
```bash
adom-desktop status
```
```json
"permissions": {
"shell": {
"autoApproved": false,
"willPrompt": true,
"source": "none", // "permanent" | "timed" | "none"
"expiresInSec": null,
"grantedBy": "human-only",
"grantVerb": "request_shell_approval",
"gatedFor": "relay (cloud-AI) callers only",
"embeddedPolicy": "standalone" // "forward-to-hd" | "auto-approve" | "standalone"
},
"bridgeExec": { "note": "bridge exec verbs gate per the bridge's declared risk field" }
}
```
If `willPrompt` is true and no human is at the machine, get the grant FIRST
(`request_shell_approval` plus asking the user) before starting the run.
## Quick reference
| Situation | What you do |
|---|---|
| `errorCode: reason_required` | Resend the SAME call plus `reason:"<one plain sentence>"` |
| `errorCode: needs_approval` | `request_shell_approval`, ask the human to click Approve, retry |
| `errorCode: human_only` | You tried to self-grant. Ask the human instead |
| `errorCode: denied_by_user` | The human said no. Do not auto-retry; ask them |
| A capture/input verb reports a missing macOS permission | Ask the user to enable Adom Bridge under System Settings > Privacy & Security (Screen Recording or Accessibility) |
| Planning an unattended run | Check `status.permissions.shell.willPrompt` first |
| Need to drop a grant | `shell_auto_approve {"duration_secs":0}` (allowed for any caller) |