/sshrequest skill¶
Default-deny, opt-in, time-boxed SSH access from Console (forge dev VM, agent hub) into personal endpoints. Trust model: Console NEVER has standing SSH into a personal machine. Each grant requires a local Yes click and auto-revokes when the window expires.
How it works (whole loop)¶
- Justin (or any Claude session on Console) types
/sshrequest <machine> for <N> hours [reason]. - The skill calls
forge/scripts/forge_ssh_grant_request.pywhich validates the machine againstforge/data/ssh-grants/machines.jsonand atomically writes a per-machine pending file. A pending older than 15 min is treated as abandoned: auto-expired, never blocks a new request. - The target machine runs a daemon that long-polls Console via SSH (
forge_ssh_grant_wait.sh, inotifywait with 60s timeout fallback). Stale pendings are expired, not served, so no surprise dialogs hours later. - Daemon pops the approval dialog (Vector: WinForms with cat photo + airhorn, 5-min auto-close; Sol: AppleScript, 5-min timeout).
- On Approve: daemon runs the local Grant script (pubkey + marker into authorized_keys, schedules revoke), writes status JSON back to Console, marks pending processed.
- On Deny / timeout: denied status, key never added.
- At expiry: revoke (Task Scheduler on Vector, detached sleep on Sol) removes ONLY the marked grant lines. Both daemons also run a catch-up revoke at startup in case the revoke missed its window (machine off/asleep).
2026-06-12 refactor (all three Console scripts rewritten, tested 25/25)¶
- Stale-pending TTL (15 min) in
forge_ssh_grant_request.py; canonical expire logic exposed as--expire-stale, used bywait.shtoo. Fixes the deadlock where an unanswered request blocked all future requests (exit 3 forever). --status [--machine X]flag: combined pending + grant status view with expiry math. The skill now uses this instead of raw cat.- Atomic JSON writes everywhere (temp + rename) so inotify watchers never read partial files.
wait.sh: inotifywait-t 60closes the startup race and acts as poll fallback; orphan guard exits when PPID becomes 1 (dropped daemon connections used to leak bash+inotifywait pairs forever; found 16 sol orphans accumulated since Jun 2).- Input validation: machine regex (path-traversal safe), nonce hex, outcome whitelist in
mark_processed. FORGE_SSH_GRANTS_DIRenv override on all three scripts for isolated testing.- Vector
Watch-ConsoleSSHRequests.ps1: WinForms dialog 5-min auto-close timer (was blocking the daemon forever if unanswered);$LASTEXITCODEcheck after Grant (PS does not throw on script exit codes; a failed grant used to report stale state.json data back as "granted"); startup catch-up revoke ported from Sol. Deployed to Vector + daemon restarted 2026-06-12.
Incident 2026-06-12: standing access for 41 days¶
The May-02 Vector grant's auto-revoke never fired (one-shot Task Scheduler task, "Interactive only"; machine off/logged-out at trigger time, one-shots do not retry). Console key sat in authorized_keys May 2 to Jun 12. Repaired via one-shot script; catch-up revoke at daemon start now prevents recurrence.
Hard-won gotchas¶
- NEVER run
Revoke-ConsoleSSH.ps1/Grant-ConsoleSSH.ps1over SSH on Vector: the*-ScheduledTaskcmdlets andStart-Jobstall indefinitely in non-interactive SSH sessions. Useschtasks.exefor task ops over SSH; the local interactive daemon can keep using the cmdlets. - pgrep/pkill -f with the wait-script name matches your own shell's command line; use explicit PIDs or bracket-pattern (
forge_ssh_grant_[w]ait) when cleaning up. - Daemon survives
schtasks /End+/Runover SSH fine (verified 2026-06-12; earlier note about daemons dying with the SSH session did not reproduce). Show-Approval.ps1on Vector is dead code (dialog is inline in the watcher); Sol'sshow-approval.shIS used.
Files (canonical homes)¶
| File | Where | Purpose |
|---|---|---|
/sshrequest skill |
forge/.claude/skills/sshrequest/SKILL.md |
User-facing slash command |
| Request writer + status + TTL | forge/scripts/forge_ssh_grant_request.py |
Pending JSON, validation, --status, --expire-stale |
| Mark-processed helper | forge/scripts/forge_ssh_grant_mark_processed.py |
Daemon calls via SSH to update Console state |
| Inotify long-poll helper | forge/scripts/forge_ssh_grant_wait.sh |
Console-side blocking script |
| Machine registry | forge/data/ssh-grants/machines.json |
vector + sol registered |
| Vector scripts | Vector C:\Users\Justi\forge-vector-local\ssh-grant\ (canonical forge/scripts/vector/ssh-grant/) |
Grant / Revoke / Get / Watch |
| Sol scripts | Sol /Users/sol/forge-sol-local/ssh-grant/ (canonical forge/scripts/sol/ssh-grant/) |
grant / revoke / get / watch / show-approval |
Windows one-time prereqs (per README in vector/ssh-grant/)¶
OpenSSH Server capability + service; network profile Private (firewall rule scope); disable the admin administrators_authorized_keys override in sshd_config so ~/.ssh/authorized_keys is honored.
[Claude Code, 2026-06-12]