- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
5.0 KiB
2. Approval grants are scoped to a program, not a process
Date: 2026-08-27
Status
Accepted. Supersedes the grant-scoping rule in docs/ideas/ssh-agent.md
("Bounded approval grants"), which this decision deliberately relaxes.
Context
The design specified that an approval grant is "scoped to one key and one
live client process", keyed on the peer PID together with that PID's start
time and the executable path captured at grant time. PID reuse therefore
cannot inherit a grant, and a re-exec invalidates it.
That rule was justified by a usability argument:
Approval strictly per signature is unusable for the workflows this feature exists to serve. A
git rebaseover twenty commits withgpg.format=sshis twenty modal prompts; a fetch followed by a push is two.
Live testing against a real vault showed the rule does not achieve that. Git
does not hold a connection to the agent across commits: it spawns a fresh
ssh-keygen -Y sign process for every commit it signs. Every one of those
has a different PID and a different start time, so a PID-scoped grant never
matches. Approving "for this process" and then making a second signed commit
prompted again, and a twenty-commit rebase would prompt twenty times whether a
grant was taken or not.
So the grant, as specified, was close to inert: it helped only a single long-lived process making repeated signature requests, which is not a workflow this feature was built for. The button existed and did nothing useful.
Decision
A grant is scoped to one key and one program, for one user: it matches on the peer UID, the executable path captured at grant time, and the public key. PID and process start time are still captured and shown in the prompt, but no longer participate in matching.
The approval button says "Approve for this program", not "for this process", because that is what it now does.
Unchanged:
- The peer UID must equal the companion's effective UID. That is the one property the companion actually verifies, and it is not relaxed here.
- The window is still bounded by
sshAgentApprovalWindowSec(default 120s, maximum 900s,0disables grants entirely). - Grants still live only in the companion's memory, never touch disk, and never survive a restart.
- Every lifecycle event that dropped a grant before still drops it: expiry,
lock, logout, account change, epoch change, disabling the feature, screen
lock, suspend,
revoke_grants, and per-grant revocation. - A grant still replaces the prompt, not the final check. Epoch, lock state and key identity are rechecked immediately before the signing primitive.
Consequences
What this accepts. During an open window, any process running the same executable path, as the same user, can obtain a signature with that key without a prompt. Under PID scoping that was limited to one process.
Why that is tolerable here. The threat model this feature works within
already states it "does not claim to protect an unlocked desktop from
arbitrary code already running as the same user". A hostile same-UID process
that wanted a signature under the old rule could simply execute
/usr/bin/ssh-keygen itself and request one in its own right — it would face
a prompt, but so would any first request under either rule. The scope change
does not hand an attacker a capability they could not otherwise reach; it
removes a distinction that cost the user twenty prompts and bought a boundary
that a same-UID attacker was never obstructed by.
What genuinely widens. The window is now shared. If the user approves
/usr/bin/ssh-keygen for two minutes to sign a rebase, a concurrent hostile
invocation of that same binary during those two minutes signs without asking.
Under PID scoping it would have prompted. This is a real reduction, and it is
the price of the feature working at all.
Mitigations retained. The window defaults to 120 seconds rather than the 900-second maximum; grants are visible in the panel with their remaining time and revocable individually or all at once; and every live grant is destroyed by a lock, a screen lock, or a suspend.
If this proves too wide, the narrower option is to keep program scoping but
additionally require that the requesting process's parent match the one that
was approved — which would cover Git's per-commit children while excluding
unrelated invocations. That was not done here because parent PIDs are as
forgeable as any other /proc metadata and would add a check that reads as a
security boundary without being one.
Alternatives considered
- Key-only grants for the window. Simplest, and matches what
ssh-agent's own confirm timeout does. Rejected as wider than necessary: the program is cheap to match on and excludes unrelated binaries. - Keep PID scoping and document the limit. Honest, but leaves a button in the UI that almost never does anything, which is its own kind of dishonesty.
- Drop grants from v1. Removes the machinery, but returns the twenty-prompt rebase the design explicitly called unusable.