dvarapala द्वारपाल
for AI agents

dvarapala

द्वारपाल · “guardian of the door”

Let AI agents ask for root commands, and run only the ones you approve.

v1.6.0 latest9 releases10 decisions recordedMIT license

The approval window: a Root access pill and Claude Code's request in the top bar, each command as a card with its reason and a checkbox, the password field with Deny and Run, and the keys in the footer

Dvarapala (द्वारपाल) is Sanskrit for "permission, consent": nothing runs as root without yours.

An AI coding agent (Claude Code, Codex, Aider, Gemini CLI, anything that can run a shell command or speak MCP) regularly needs something done as root: install a package, enable a service, edit a file in /etc. The usual answer is to copy sudo commands between the agent and a terminal. dvarapala replaces that: the agent sends the commands, each with a reason; a window shows them on your desktop; you untick what you don't want, type your password once, and the ticked commands run. The agent gets every command's output and exit code back.

The same goes for commands that don't need root but that the agent can't run itself: its sandbox blocks the network or a folder, or you've set it to ask first. The agent marks those "as": "user"; they show in the same window with a YOU badge instead of ROOT, run as you with a click (no password), and come back the same way.

#Features

#Install

Requirements: Linux with a Wayland desktop (sway, Hyprland, GNOME, KDE…) and systemd's user manager, sudo, Python 3.11+, PyGObject, GTK 4, libadwaita, and libnotify (notify-send).

git clone https://github.com/Theyashsawarkar/dvarapala ~/.local/share/dvarapala
~/.local/share/dvarapala/install.sh          # asks for sudo once
dvarapala --why "Test" --cmd "id"            # the window opens; approve it

install.sh installs everything root-owned (/usr/local/bin/dvarapala, /usr/local/lib/dvarapala/), so an agent running as your user can't change what the window shows or what it runs. On Arch it installs the dependencies too; elsewhere install them first (Debian/Ubuntu: python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 libnotify-bin; Fedora: python3-gobject gtk4 libadwaita libnotify).

~/.local/share/dvarapala/install.sh --check       # what differs from this clone
git -C ~/.local/share/dvarapala pull && ~/.local/share/dvarapala/install.sh   # update
~/.local/share/dvarapala/install.sh --uninstall

It used to be called vayu-elevate; the installer keeps that name as a link to dvarapala, so agents and notes that still use it keep working.

#What you see

A window opens with everything the agent asked for: each command exactly as it will run, its reason, a checkbox, and a badge saying how it runs: ROOT (as root, from /) or YOU (as you, in the folder shown under it). The top bar says how many of each. Untick anything you don't want, type your password if any ROOT command is ticked, press Run (or Enter). A request with only YOU commands has no password field. The window closes the moment you've answered (after a right password, if one was needed) and you're done: the ticked commands run on without it, in order, stopping at the first failure, and it's the agent that waits for the results, failures included (ADR 9).

It looks like the rest of the desktop: Vyom's panels, in your varna theme when you have one. The theme's colours are used only if everything stays readable (see Security), so a theme can change tints, never hide anything.

KeyAction
Enter (password field or Run)Run the selected commands (as root and as you)
SpaceToggle the focused command
EscCancel the root request (deny everything)
Ctrl+HHide the approval window (unanswered, the request stays open on the phone until it expires)

Closing the window, Deny or Esc means nothing runs. Three wrong passwords end the request. A request nobody answers expires after 10 minutes.

#For agents: sending a request

Send a JSON request on stdin. It blocks until the user answers (up to 10 minutes), then prints a JSON result.

dvarapala <<'EOF'
{
  "requester": "Claude Code",
  "reason": "Install Docker and start it on demand, as you asked.",
  "commands": [
    {"cmd": "pacman -S --needed --noconfirm docker", "why": "Install Docker"},
    {"cmd": "systemctl enable --now docker.socket", "why": "Start Docker when something connects to it"},
    {"cmd": "docker compose up -d", "why": "Start the project's services",
     "as": "user", "cwd": "/home/me/project"}
  ]
}
EOF

One-off form: dvarapala --requester "Claude Code" --why "Reload udev rules" --cmd "udevadm control --reload" (repeat --why/--cmd pairs for more; add --as user and --cwd DIR to run them all as you). dvarapala --schema prints the request and result JSON Schema.

Rules for the commands:

Ask for exactly what's needed, explain each command honestly, don't re-send a denied request unless the user asks, and treat a declined command as the user's answer.

#For agents: reading the result

{
  "status": "completed",
  "message": "Ran the approved commands.",
  "approved": [0, 1],
  "declined": [],
  "results": [
    {"index": 0, "command": "pacman -S --needed --noconfirm docker", "why": "Install Docker",
     "as": "root", "cwd": "/", "decision": "approved", "state": "ok", "exit_code": 0, "stdout": "...", "stderr": "",
     "duration": 14.2, "timed_out": false, "truncated": false}
  ]
}
statusMeaning
completedAt least one command was approved and they ran: check each result's state
deniedThe user pressed Deny, Esc or closed the window; nothing ran
timeoutNo answer within 10 minutes; nothing ran
auth_failedWrong password three times; nothing ran
errorBad request or the tool itself failed; see message
stateMeaning
okRan, exit code 0
failedRan, non-zero exit code
skippedApproved, but an earlier command failed
declinedThe user unticked it
not_runThe request was denied, timed out or failed before running

Output is capped at 512 KB per stream (truncated: true when cut).

Exit codeMeaning
0Every command was approved and succeeded
1An approved command failed
2Bad request or tool error
3Nothing ran (denied, timeout, wrong password)
4Some commands were declined; the approved ones succeeded

#For agents: over MCP

dvarapala --mcp is an MCP server (stdio) with one tool, request_commands, taking the same request object and returning the result as text and structuredContent. It's also listed under its 1.x name, request_root_commands (the same tool: commands without "as" run as root), so existing setups keep working.

claude mcp add --scope user dvarapala -- dvarapala --mcp      # Claude Code
{
  "mcpServers": {
    "dvarapala": {"command": "/usr/local/bin/dvarapala", "args": ["--mcp"]}
  }
}

The call blocks until the user answers (up to 10 minutes) and the approved commands finish; the window itself closes as soon as the user answers. If your client times out tool calls sooner, raise its limit (Claude Code: the MCP_TOOL_TIMEOUT environment variable, in milliseconds).

#Telling your agent about it

Add a few lines to the agent's global instructions (Claude Code: ~/.claude/CLAUDE.md; Codex: ~/.codex/AGENTS.md; Gemini CLI: ~/.gemini/GEMINI.md):

- Commands you can't run yourself: don't ask me to paste them into a
  terminal. Send them to `dvarapala` (JSON on stdin: requester, reason,
  commands: [{cmd, why, as, cwd}]; see `dvarapala --help`): `"as": "root"`
  for anything that needs root, `"as": "user"` (with `cwd`) for the rest,
  like a command your sandbox blocks. I approve them in a window; you get
  the results. Respect declined commands; never retry a denied request
  unasked.

#What's recorded

Every command that ran as root, with its exit code, goes to /var/log/dvarapala.log: root-only, so the agent can't edit it. Every request, denied ones included, goes to your own ~/.local/state/dvarapala/requests.jsonl, with each command's as and cwd (YOU commands are only in this one).

sudo tail -n 20 /var/log/dvarapala.log | jq .
tail -n 5 ~/.local/state/dvarapala/requests.jsonl | jq .

#Security

dvarapala is a consent gate: nothing runs as root unless you see it and type your password, and nothing runs as you unless you see it and press Run.

GuaranteeHow
The agent never sees your passwordIt's typed into the approval window: a separate process the agent didn't start (a transient systemd user service, with a clean environment) and can't inspect (PR_SET_DUMPABLE=0).
What runs is what you sawThe window and the root-side runner are root-owned files; the window holds the request in memory once shown; Python runs isolated (-I); your GTK styling can't hide part of it (the window uses root-owned XDG dirs); the theme's colours are read as plain #rrggbb only and used only while every text stays readable, else dvarapala's own (ADR 6); invisible or direction-changing characters are refused.
No leftover accesssudo runs with -k both times: no cached credentials for anything else.
ROOT and YOU can't be confusedEach card says which, in its own colour, with its number in the run order; YOU cards show the user and folder. A command without "as" is ROOT, never the reverse; root commands always run from /.
A YOU command runs only what you sawIt runs from the window, not the agent, with bash -c (no login shell, so no rc file), in an allowlisted environment taken from the session's systemd user manager, not the agent: no BASH_ENV, LD_PRELOAD or exported functions, and a PATH without relative or world-writable folders (ADR 8).
Answering from the phone changes none of thisThe phone's answer, through ekatva's end-to-end encrypted link, ticks the commands visibly and fills the password field; then the same check and runner. The window sends nothing secret out, and finds ekatva by your uid, not the agent's environment (ADR 7).

What it can't do: protect you from a program that is actively malicious and already running as your user. Such a program could draw a lookalike window to collect your password, or tamper with the systemd user manager the window starts from; that's true of every password prompt on a Linux desktop, polkit's included. Use dvarapala with agents you trust to ask honestly, and read each command before you approve it. Report security problems privately: SECURITY.md.

#How it works

  1. dvarapala rejects bad requests (no why, more than 50 commands, hidden characters), writes the request to $XDG_RUNTIME_DIR/dvarapala/<id>/ (mode 0700) and starts the window with systemd-run --user and env -i: a transient service, not a child of the agent.
  2. The window (lib/dialog.py, GTK 4 / libadwaita) reads the request once into memory and shows it. You untick, type your password, press Run.
  3. If a ROOT command is ticked, it checks the password with sudo -S -k true. Then the window hides (the process stays, with no window) and it goes through the ticked commands in order: each run of consecutive ROOT commands goes to sudo -S -k -- python3 -I runner.py, with the password and those commands on its stdin (nothing is written to disk); each YOU command runs in the window's own process (lib/userrun.py).
  4. The runner (lib/runner.py) runs each command with bash -c from /, no stdin, a minimal environment, 30 minutes max each, and appends each to /var/log/dvarapala.log. YOU commands get the same limits. The first failure, either kind, skips the rest.
  5. The process writes result.json and exits; dvarapala prints it and exits.

#Troubleshooting

#Contributing

Bug reports, ideas and pull requests are welcome: see CONTRIBUTING.md, and CHANGELOG.md for what changed in each release. Changes to what runs as root get extra scrutiny; security reports go privately (SECURITY.md).

dvarapala comes from Vayu, a keyboard-driven Wayland desktop, and works on any Linux desktop on its own.

#License

MIT