Skip to content

/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)

  1. Justin (or any Claude session on Console) types /sshrequest <machine> for <N> hours [reason].
  2. The skill calls forge/scripts/forge_ssh_grant_request.py which validates the machine against forge/data/ssh-grants/machines.json and atomically writes a per-machine pending file. A pending older than 15 min is treated as abandoned: auto-expired, never blocks a new request.
  3. 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.
  4. Daemon pops the approval dialog (Vector: WinForms with cat photo + airhorn, 5-min auto-close; Sol: AppleScript, 5-min timeout).
  5. On Approve: daemon runs the local Grant script (pubkey + marker into authorized_keys, schedules revoke), writes status JSON back to Console, marks pending processed.
  6. On Deny / timeout: denied status, key never added.
  7. 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 by wait.sh too. 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 60 closes 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_DIR env 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); $LASTEXITCODE check 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.ps1 over SSH on Vector: the *-ScheduledTask cmdlets and Start-Job stall indefinitely in non-interactive SSH sessions. Use schtasks.exe for 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 + /Run over SSH fine (verified 2026-06-12; earlier note about daemons dying with the SSH session did not reproduce).
  • Show-Approval.ps1 on Vector is dead code (dialog is inline in the watcher); Sol's show-approval.sh IS 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]