Skip to content

Explain polkit commands with the default coding agent on request - #11984

Open
cristim wants to merge 4 commits into
omacom:quattrofrom
cristim:polkit-ai-explain
Open

cristim wants to merge 4 commits into
omacom:quattrofrom
cristim:polkit-ai-explain

Conversation

@cristim

@cristim cristim commented Sep 15, 2026

Copy link
Copy Markdown

Builds on #11983 (which builds on #11795); the change for review is the last commit, "Explain polkit commands with the default coding agent on request".

What

On a pkexec prompt, pressing Tab or clicking the hint asks your default coding agent what the command likely does. The answer shows in the prompt as Claude's guess, which may be wrong: ... (or Codex's, or Pi's). Nothing is sent until you ask. omarchy agent explain "<command>" does the same from a terminal.

Claude Code, Codex and Pi are supported, because they can run one turn with no tools. Other default agents get no hint.

Treating the command as hostile

Whatever asked for root wrote that command, so:

  • It goes in as fenced data, with invisible characters removed and every @ shown as a fullwidth one (some agents attach @path as a file before any tool runs). The prompt tells the agent never to follow instructions in it and to call out text claiming to be safe.
  • The agent runs one turn with no tools, MCP servers, hooks, context files or saved session, and never installs or updates anything.
  • The full, shell-quoted command from omarchy-polkit-caller is used when the lookup found it; otherwise the agent is told quoting was lost or part of the command is missing.

Sandbox

The agent also runs inside bubblewrap under a Landlock scope:

  • It sees /usr, /etc, /opt, mise's installs and its own binary read-only, a private home and /tmp, and nothing of your files or /run/user.
  • Landlock blocks abstract unix sockets (such as Xwayland's X11 socket, which bubblewrap's shared network would otherwise expose) and signals to anything outside.
  • Only locale, proxy, certificate and agent variables (such as ANTHROPIC_* or *_API_KEY) reach it, and no inherited file descriptors.
  • It works on a copy of its login. A refreshed login goes back only if it is one small JSON object, a plain file read once outside the sandbox's reach, and the real login hasn't changed meanwhile.
  • If the sandbox can't start (no bubblewrap, no user namespaces, Linux older than 6.12 or Landlock disabled), the hint doesn't appear.

bubblewrap joins the base packages, with a migration for existing installs (it's already there as a dependency of glycin).

Known limits

  • The agent keeps network access to reach its provider, which also reaches services listening on localhost.
  • Code that somehow ran inside the sandbox could swap in a login for another account, since any valid JSON object is accepted.
  • A login refreshed while you close the prompt mid-answer isn't saved back.
  • Agents installed outside mise, /usr or /opt as more than a single file may fail inside the sandbox; the prompt then shows the agent's error.

Tests

  • test/shell.d/agent-explain-test.sh: flags, prompt fencing and cleaning, locale-safe caps, errors and timeouts, stopping, mise installs, and a real bubblewrap section (skipped where it can't run) checking hidden files, blocked abstract sockets, environment filtering, closed descriptors, and login copy-back against broken, doubled, oversized, unreadable, locked, symlinked, hard-linked and swapped copies.
  • polkit-caller-test, polkit-test, qml-text-format-test, bin-style-test and ./test/cli pass.
  • Tried live with Claude Code as the default agent: an answer in about 8 seconds, the real login untouched.

Screenshots

Before (requester details from #11983):

Polkit prompt before the explanation hint

After — explanation available on request:

Polkit prompt with the Tab hint

While asking the default agent:

Polkit prompt waiting for the agent

With the answer (long explanations are limited to seven lines and end with an ellipsis):

Polkit prompt showing the agent explanation

🤖 Generated with Claude Code

https://claude.ai/code/session_015vSTTwVkpo2cevibWvt5iB

cristim and others added 4 commits September 14, 2026 12:58
pkexec's message quotes the command line with a backtick and a closing
apostrophe, so a command that contains its own quotes (bash -c 'echo hi')
fell through the label parser and the full raw sentence overflowed the
screen. Parse the message by its fixed tail instead, and render the
request inside the card: a "Run as root" / "Run as <user>" title, then
the program in the accent color with its arguments wrapping below.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vSTTwVkpo2cevibWvt5iB
pkexec joins the command line without escaping it, so an argument can
carry line breaks or bidi controls. With the 4-line cap on the arguments,
line breaks pushed the rest of the command out of the prompt, and a
right-to-left override rendered harmless text as a different command.
Escape those characters in the program, arguments and target user, and
show the arguments uncapped, since pkexec already limits them to about
80 bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNhNwBijbgSaQ3KLmKbGbo
pkexec's message says what will run, not who asked for it, and
Quickshell's polkit agent doesn't pass on polkit's subject pid. The
prompt now asks omarchy-polkit-caller, which finds the pkexec behind the
prompt in /proc and names the processes above it, as in
"omarchy-update ← bash ← foot".

The requester chooses its pkexec's name, options and program, so none of
those are trusted. Every root process this user started counts as the
pkexec unless the message pkexec would show for its command can't end
the way the shown one does, and two such processes mean no answer. The
one left must render exactly the shown message; the helper then also
returns the command as pkexec received it, shell-quoted.

Processes choose their own names, so the line reads "Not verified:
requested by …". Control, line-separator, bidi and zero-width
characters are stripped and long names cut, so nothing can hide that or
break up the line. The lookup runs under a short timeout and a result
that arrives after a newer prompt started is ignored, so a slow or stuck
lookup only ever leaves the line out.
Pressing Tab on a pkexec prompt, or clicking the hint, asks the default
coding agent what the command likely does, through
omarchy-agent-explain. The answer shows in the prompt as that agent's
guess. Nothing is sent until the user asks, and the full command from
omarchy-polkit-caller is used when the lookup found it.

Whatever asked for root wrote that command, so it goes in as fenced data
with invisible characters removed and every @ shown as a fullwidth one,
since some agents attach "@path" as a file. The agent runs one turn with
no tools, MCP servers, hooks or context files; only agents that can run
that way get the hint.

The agent also runs inside bubblewrap under a Landlock scope. It sees the
system and its own install read-only, a private home and /tmp, only
agent-related environment variables and no inherited files, and it can't
reach abstract unix sockets such as X11's or signal anything outside. It
works on a copy of its login, and a refreshed login goes back only if it
is one small JSON object and the real login hasn't changed meanwhile.
bubblewrap joins the base packages, with a migration for existing
installs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant