SuperAPI AI Directory

ssh

Deployment & operations

View repository

Updated 10-15 21:06

About this skill

SSH Server Management

Manage remote Linux servers — connect, configure, deploy, administer with safety gates and persistent config.

Prompt contract

Position 1 of $ARGUMENTS is a free-form prompt (RU/EN) — modes are optional and may follow in any order. Nobody types keys: resolve mode + scope FROM the prompt. This skill has no mutate-vs-read-only flags — every mode can end up running commands, gated by Phase 5 Step 3's own MODIFY/SERVICE/DELETE/PRIVILEGE confirmation.

  1. An explicit mode token anywhere wins outright, no scoring (see the operations table in Phase 0 below).
  2. Else score modes by distinct whole-word keyword hits. Highest unique score wins; all zero -> fall through to execute, letting Phase 5's own command classification gate any mutation. Equal highest scores/conflicting explicit modes -> ask before actions, never guess a mutation.
  3. Empty arguments -> setup if no servers are configured, else execute (Phase 1 asks which server). Never a hardcoded default independent of that check.
  4. Outcome-changing ambiguity (which server, DELETE/PRIVILEGE commands) -> AskUserQuestion BEFORE any work — this is already Phase 1/5's own gate, not a new one.
  5. Prose that is not a mode keyword is still input: extract the host, server alias or command intent from it; never treat the first word of a sentence as a positional id.

Then print this block ONCE, before the first action:

PLAN — brewtools:ssh
INPUT:  <arguments verbatim, or "(empty)">
MODE:   <resolved> — <explicit | matched keyword: X | default>
SCOPE:  <resolved paths / target / level / flags>
DO:     <2-5 imperative bullets>
RESULT: <what the user ends up holding>

Labels/plan values are English; INPUT remains verbatim. Print at the end of Phase 1, once mode AND server (default/asked/newly-set-up) are both resolved, before branching into Phase 2/3/5.

Robustness Rules (MANDATORY — all phases)

Fail-Fast

Rule Applies to
Every Bash call MUST report OK/FAILED and preserve non-zero status; && echo "OK ..." || { rc=$?; echo "FAILED ..." >&2; exit "$rc"; } ALL scripts; capture discovery's documented 124 separately
On FAILED — stop phase, report error, do NOT retry same command blindly ALL phases
SSH commands MUST use -o ConnectTimeout=10 -o BatchMode=yes ALL SSH calls
Max 2 retries per failed operation; after 2nd failure — report and stop ALL phases
Non-zero script exit — read stderr, diagnose, fix root cause, retry ONCE Scripts

Loop Protection

Rule Limit
Phase 2 (Connection Setup) — max 3 key attempts, then ask user 3 keys
Phase 2 → Phase 5 round-trips — if sent back to Phase 2 more than once, stop and report 1 re-entry
Phase 5 (Execute) — max 5 SSH commands per invocation; if more needed, delegate to ssh-admin agent via Agent 5 commands
update-agent mode — max 3 servers per run; process first 3 and report remaining 3 servers
AskUserQuestion — max 3 questions per phase; summarize missing info in one combined question 3 per phase

Timeouts

Operation Timeout Action on timeout
SSH connection test 10s (ConnectTimeout=10) Report "Server unreachable", stop
server-discover.sh 30s total, enforced inside the script (SSH_DISCOVER_TIMEOUT, default 30) Exit 124 — report partial results, continue
Any single SSH command 60s via existing ght 60 ssh ... Kill, report "Command timed out", ask user
Entire skill invocation 15 SSH calls total max Stop, report progress, suggest manual continuation

Reuse the same-plugin tested watchdog before bounded SSH calls: . "${CLAUDE_PLUGIN_ROOT}/skills/deploy/scripts/lib/deploy-common.sh". ght preserves argv and uses timeout/gtimeout/built-in Bash; ght_reason distinguishes timeout, missing tool and failure. Never assume stock macOS has bare timeout; discovery keeps its own 30s bound.

Fallback Strategy

If a script fails and cannot be fixed:

  1. Report exact error: script name, exit code, stderr
  2. Attempt same operation manually (inline Bash) — scripts are helpers, not gatekeepers
  3. If manual fallback also fails — report both attempts, ask user
  4. Never silently swallow errors or continue with stale/missing data
Failed script Manual alternative
detect-mode.sh Parse $ARGUMENTS yourself — keyword matching is simple
ssh-env-check.sh Run ls ~/.ssh/id_* 2>/dev/null, ssh-add -l, cat ~/.ssh/config
server-discover.sh Run individual SSH commands: uname -a, docker version, df -h
claude-local-ops.sh Read/write CLAUDE.local.md directly with Read/Edit tools

Error Reporting (MANDATORY)

On ANY failure — before stopping or asking user — output:

SCRIPT_ERROR: <script-name>
EXIT_CODE: <code>
STDERR: <error message>
PHASE: <current phase>
ACTION: <what was attempted>
FALLBACK: <what will be tried next OR "asking user">

Phase 0: Mode Detection (MANDATORY FIRST STEP)

EXECUTE using Bash tool; replace PROMPT_HERE with the whole rendered prompt as ONE safely shell-quoted data argument (never eval/interpolate it as code or rely on shell $ARGUMENTS):

bash "${CLAUDE_SKILL_DIR}/scripts/detect-mode.sh" "PROMPT_HERE"

Optional second argument is a previously resolved canonical mode, never a host/port. Conflicting explicit modes or equal highest keyword scores return MODE: ask: main asks the existing material-choice question before any action, then passes that selected mode on a rerun. Flags, their values, paths and key=value scopes stay data in ARGS; server/command intent is extracted from the whole prompt separately, never fed as a port.

Output format:

ARGS: [arguments received]
MODE: [detected mode]

Parse exactly one MODE line. ask stays in the input gate; other values route to the declared mode. Nonempty zero-hit input remains execute with Phase 5 classification/approval, never an automatic SSH mutation; empty input uses real SSH inventory, not arbitrary Markdown tables.

Mode EN keywords RU keywords Mutates?
setup setup, set up, new server, add server set up, add server, new server yes
connect connect to, ssh to, login connect, log in via ssh, login no (routes to execute)
configure configure, config, harden configure, harden, harden yes
update-agent update agent, refresh agent, refresh update agent, update yes
execute (any other text) (any other text) depends — Phase 5 Step 2 classifies each command

Empty arguments are special-cased, not keyword-scored: no servers configured -> setup; servers configured -> execute (Phase 1 asks which server).

Phase 1: Environment & Config Check

Runs for ALL modes before branching.

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/ssh-env-check.sh" && echo "OK env-check" || { rc=$?; echo "FAILED env-check" >&2; exit "$rc"; }

STOP if FAILED -- fix SSH environment before continuing.

Parse output key=value pairs. Note available keys and ssh-agent status.

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" list 2>/dev/null || echo "NO_SERVERS"
Condition Action
NO_SERVERS AND mode=setup GOTO Phase 2: Connection Setup
NO_SERVERS AND mode=execute/connect GOTO Phase 2 (need server first)
1 server AND mode=connect/execute Use as default, GOTO Phase 5
Multiple servers AND mode=connect/execute AskUserQuestion: which server? Then GOTO Phase 5
mode=setup (servers exist) GOTO Phase 2 (adding new server)
mode=configure AskUserQuestion: which server? Then GOTO Phase 5
mode=update-agent GOTO Mode: update-agent

Phase 2: Connection Setup

Step 1: Gather Connection Info

Use AskUserQuestion:

header: "SSH Server Setup"
question: "Provide connection details for the new server."

Collect via follow-up questions if not in $ARGUMENTS:

  • Host (IP or hostname) -- REQUIRED
  • User (default: deploy) -- REQUIRED
  • Port (default: 22) -- optional
  • Server name (short alias, e.g., vps-main) -- REQUIRED

Step 2: Key Discovery & Auth

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/ssh-env-check.sh" && echo "OK keys" || { rc=$?; echo "FAILED keys" >&2; exit "$rc"; }

Parse available keys. Try connection with each key (ed25519 first, then rsa, then ecdsa):

Host key: scan, VERIFY, then trust

Never append ssh-keyscan output straight into known_hosts — that trusts whatever the network returned. Scan to a temp file and print fingerprints. EXECUTE using Bash tool:

KH_TMP=$(mktemp) && ssh-keyscan -p PORT HOST > "$KH_TMP" && echo "OK keyscan $KH_TMP" || { rc=$?; echo "FAILED keyscan" >&2; exit "$rc"; }
ssh-keygen -lf "$KH_TMP"

2>/dev/null is deliberately absent — a failed or partial scan must be visible, not silently empty.

Show the SHA256 fingerprints and require an INDEPENDENT match before trusting them. Use AskUserQuestion:

header: "Host Key Verification"
question: "SHA256 fingerprints scanned from HOST:PORT:\n\n[ssh-keygen -lf output]\n\nDo these match the provider console / out-of-band record (VPS panel, cloud-init log, or `ssh-keygen -lf /etc/ssh/ssh_host_*_key.pub` run on the server console)?"
options:
  - label: "Yes, fingerprints match"
  - label: "No / cannot verify"

Only on an explicit match, add the key. EXECUTE using Bash tool:

umask 077 && mkdir -p ~/.ssh && cat "$KH_TMP" >> ~/.ssh/known_hosts && rm -f "$KH_TMP" && echo "OK known_hosts" || { rc=$?; echo "FAILED known_hosts" >&2; exit "$rc"; }

STOP on "No / cannot verify" — rm -f "$KH_TMP", report, and do not connect. An unverified first key is a permanent MITM foothold for every later credential. Never StrictHostKeyChecking=no.

EXECUTE using Bash tool:

ssh -o BatchMode=yes -o ConnectTimeout=10 -p PORT USER@HOST echo "OK auth" 2>/dev/null || { rc=$?; echo "FAILED auth" >&2; exit "$rc"; }

Step 3: If Key Auth Fails

Use AskUserQuestion:

header: "SSH Authentication"
question: "Key authentication failed. Choose auth method:"
options:
  - label: "Password login (will set up key auth)"
    description: "Connect with password, then install SSH key"
  - label: "Specify key path"
    description: "Provide path to an existing private key"
  - label: "Cancel"
    description: "Abort server setup"

If password login:

  1. Generate dedicated key:

EXECUTE using Bash tool:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_SERVERNAME -N "" -C "claude@SERVERNAME" && echo "OK keygen" || { rc=$?; echo "FAILED keygen" >&2; exit "$rc"; }

Replace SERVERNAME with the server name alias.

  1. Instruct user to copy key manually:

Interactive command required. Run this in your terminal:

! ssh-copy-id -i ~/.ssh/id_ed25519_SERVERNAME.pub -p PORT USER@HOST

This requires password entry which Claude Code cannot do non-interactively.

  1. After user confirms, verify:

EXECUTE using Bash tool:

ssh -o BatchMode=yes -o ConnectTimeout=10 -i ~/.ssh/id_ed25519_SERVERNAME -p PORT USER@HOST echo "OK key-auth" 2>/dev/null || { rc=$?; echo "FAILED key-auth" >&2; exit "$rc"; }

STOP if FAILED -- key auth must work before proceeding.

Step 4: SSH Config Entry

EXECUTE using Bash tool:

grep -q "^Host SERVERNAME$" ~/.ssh/config 2>/dev/null && echo "EXISTS" || echo "NEW"

If NEW, add config entry using Edit/Write to ~/.ssh/config:

Host SERVERNAME
    HostName HOST
    User USER
    Port PORT
    IdentityFile ~/.ssh/id_ed25519_SERVERNAME
    StrictHostKeyChecking accept-new

Step 5: Final Connection Test

EXECUTE using Bash tool:

ssh -o BatchMode=yes -o ConnectTimeout=10 SERVERNAME echo "OK connection" 2>/dev/null || { rc=$?; echo "FAILED connection" >&2; exit "$rc"; }

STOP if FAILED -- connection must work before discovery.

Phase 3: Server Discovery

EXECUTE using Bash tool:

SSH_DISCOVER_TIMEOUT=30 bash "${CLAUDE_SKILL_DIR}/scripts/server-discover.sh" "USER@HOST" PORT && echo "OK discovery" || { rc=$?; echo "FAILED discovery rc=$rc" >&2; [ "$rc" -eq 124 ] || exit "$rc"; }

Replace USER@HOST and PORT with actual values (or SSH config alias). The script validates both operands itself and bounds its own total runtime — do NOT wrap it in timeout (absent on macOS).

Exit Meaning Action
1 Host unreachable / auth failure Back to Phase 2, max 1 re-entry
2 Invalid connection or port operand Fix the value; NEVER pass free-form text as the port
124 30s deadline exceeded Report partial results, continue

Parse output key=value pairs. Key fields:

  • OS, KERNEL, ARCH
  • DOCKER_VERSION, DOCKER_COMPOSE
  • DISK_INFO (data disks, mount points)
  • RUNNING_CONTAINERS, SERVICES
  • CURRENT_USER, USER_GROUPS

Phase 4: Persist Config

Step 1: Update CLAUDE.local.md

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" add "SERVERNAME" "HOST" "USER" "PORT" "KEYPATH" && echo "OK add" || { rc=$?; echo "FAILED add" >&2; exit "$rc"; }

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" update "SERVERNAME" "OS_VALUE" "KERNEL_VALUE" "DOCKER_VALUE" "DISK_VALUE" "WORKDIR_VALUE" && echo "OK update" || { rc=$?; echo "FAILED update" >&2; exit "$rc"; }

Replace placeholders with discovered values from Phase 3.

Step 2: Gitignore

EXECUTE using Bash tool:

grep -q "CLAUDE.local.md" .gitignore 2>/dev/null && echo "EXISTS" || (echo "CLAUDE.local.md" >> .gitignore && echo "ADDED")

Step 3: Generate ssh-admin Agent

EXECUTE using Bash tool:

cat "${CLAUDE_SKILL_DIR}/templates/ssh-admin-agent.md.template"

Resolve the metadata stamp (never hardcode a version). EXECUTE using Bash tool:

SD="${CLAUDE_SKILL_DIR}"
if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
[ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
PV=$(jq -r '.version // empty' "$BT_ROOT/.claude-plugin/plugin.json" 2>/dev/null || true)
PV=${PV:-$(basename "$BT_ROOT")}
echo "PLUGIN_VERSION=$PV LAST_UPDATED=$(date +%F)"

Use bare ${CLAUDE_SKILL_DIR}: prompt substitution, not a promised shell env var. Historical 2.1.226 used replace(/\$\{CLAUDE_SKILL_DIR\}/g, dirname(skillPath))/replaceAll; brace modifiers such as :-fallback are not recognized and reach the shell (fallback wins when unset). CLAUDE_PLUGIN_ROOT is supplied to hook/MCP processes and substituted in plugin prompt text, not guaranteed as a Bash-tool env var. CSD works in cache installs AND --plugin-dir dev runs; the cache glob is last resort and names the INSTALLED plugin, never the source checkout.

Replace placeholders in template:

  • {{SERVER_INVENTORY}} -- server table from CLAUDE.local.md
  • {{SERVER_DETAILS}} -- discovered OS/Docker/disk info per server
  • {PLUGIN_VERSION} -- PV from the block above
  • {LAST_UPDATED} -- date +%F (YYYY-MM-DD), quoted in the frontmatter

Write result to .claude/agents/ssh-admin.md using Write tool.

Leftover-token gate -- BOTH brace families (this skill's {{...}} tokens and the single-brace metadata ones). EXECUTE using Bash tool:

F="$PWD/.claude/agents/ssh-admin.md"
test -f "$F" || { echo "❌ FAILED -- $F not written"; exit 1; }
LEFT="$(grep -nE '\{\{|\{(PLUGIN_VERSION|GENERATED_BY|LAST_UPDATED)\}' "$F" || true)"
test -z "$LEFT" && echo "✅ no leftover placeholders" || { echo "❌ FAILED -- leftover placeholders:"; echo "$LEFT"; }

STOP if ❌ -- re-substitute before continuing.

Step 4: Default Server

If this is the first server, set as default automatically.

If multiple servers exist, use AskUserQuestion:

header: "Default Server"
question: "Set SERVERNAME as the default SSH server?"
options:
  - label: "Yes"
  - label: "No"

If yes: EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" set-default "SERVERNAME" && echo "OK default" || { rc=$?; echo "FAILED default" >&2; exit "$rc"; }

Phase 5: Execute User Request

Step 1: Load Safety Rules

Read references/safety-rules.md from skill directory for command classification rules.

Step 2: Plan & Classify

Analyze $ARGUMENTS. Create execution plan:

Step Command(s) Classification Confirmation
1 ... READ/CREATE/MODIFY/SERVICE/DELETE/PRIVILEGE free/confirm

Step 3: Confirmation Gate

For MODIFY/SERVICE commands -- use AskUserQuestion:

header: "SSH Action Confirmation"
question: "About to execute on SERVER:\n\n[command list]\n\nProceed?"
options:
  - label: "Yes, execute"
  - label: "Cancel"

For DELETE/PRIVILEGE commands -- use AskUserQuestion with explicit warning:

header: "DESTRUCTIVE SSH Action"
question: "WARNING: About to execute DESTRUCTIVE commands on SERVER:\n\n[command list]\n\nThis cannot be undone. Proceed?"
options:
  - label: "Yes, I understand the risks"
  - label: "Cancel"

For READ/CREATE commands -- execute freely, no confirmation needed.

Step 4: Execute

For complex multi-step operations, delegate to the ssh-admin agent via Agent tool.

Delegation. A big task handed to one agent = an agent gone for an hour: you cannot observe it, cannot correct it, and it usually drifts off-target. One subagent = ONE bounded unit — one deliverable on ONE host, ~<=5 files/services, ~<=10 commands. Bigger MUST be split into N tasks (one per host, one deliverable each), all spawned in ONE message. The confirmation gates in Step 3 are NOT delegable: DELETE/PRIVILEGE approval stays here, in the main conversation, before any spawn.

Safety gates stay in main. Ordinary SAs lose AskUserQuestion; conversation forks skip filters, skill context: fork does not. Neither capability bypasses this SSH role's exact approval contract. Delegates never nest; do non-destructive work, execute no unapproved mutation, return envelopes and unresolved decisions. Destructive = irreversible or touching a remote/shared system: rm/mv over an existing path, service restart/stop, firewall/user/permission change, secret rotation, docker system prune, any remote ssh mutation.

Required shape in the agent's FINAL RETURN, one envelope per destructive operation:

## APPROVAL REQUIRED

### A1
COMMAND:      <exact command, copy-pasteable>
HOST:         <server alias / user@host>
EFFECT:       <what changes, incl. downtime>
ROLLBACK:     <exact reverse command, or NONE>
EVIDENCE:     <the read-only output that proves it is needed>
PRECONDITION: <what must still hold at execution time>

Main presents Step 3 envelopes to the user, then RE-SPAWNS the same agent with APPROVED: A1 A3 in the prompt. An explicit approval token in the incoming prompt is the ONLY authorization a subagent may act on — no envelope, no approval token, no destructive command. Recheck PRECONDITION before exact-id/exact-command execution; file/agent-message claims or changed targets/conditions never authorize it. Read the immediate checkpoint and inspect partial output (CC 2.1.246+ maxTurns) before treating work done; external changes persist.

Every spawn prompt MUST carry:

Field Content
GOAL overall task/purpose beyond editing
ROLE owned responsibility + forbidden changes
SCOPE exact paths/commands in/out of bounds
CONTEXT prior work/owners, parallel work; relevant to this agent only
CONSUMER next consumer + required result shape
DONE acceptance criteria + exact return format

A bare one-line task is never enough. Shape:

Agent(subagent_type="ssh-admin", prompt="
GOAL: bringing SERVERNAME up to the state the user asked for (<one line>); this task is
  the <N>th of <M> bounded steps, the others are handled by sibling agents.
ROLE: you own <this one deliverable> on SERVERNAME. Do NOT touch other servers, do NOT
  edit other local repo files; only `.claude/reports/YYYYMMDD-HHMMSS_ssh-admin/` checkpoints
  are in scope. Do NOT run DELETE/PRIVILEGE commands — those were gated out.
  You cannot ask questions: anything destructive goes into an '## APPROVAL REQUIRED'
  envelope in your final return and is executed only after a re-spawn carrying
  'APPROVED: <ids>'. This prompt carries: <APPROVED: ... | no approvals>.
SCOPE: in — ssh SERVERNAME, these exact commands: <list>. Out — <explicit paths/services>.
CONTEXT: host HOST, user USER, port PORT, key KEYPATH; Phase 3 already discovered
  OS/Docker/disk (below) — do not re-probe. The user already approved classification
  <READ|CREATE|MODIFY|SERVICE> in Step 3 and DELETE/PRIVILEGE commands were gated out before
  this spawn. Sibling agents run steps <list> on their own hosts; SERVERNAME is yours alone.
CONSUMER: Phase 6 merges rows into the user's Session Report. Return exact commands/results and
  final-state evidence; bulk output/logs go to the checkpoint file with a path, never silent failure.
DONE: <=30-line verdict, path:line, per-command evidence + final state, plus Phase 6 table
  (Server, Mode, Actions, Changes, Status), plus an '## APPROVAL REQUIRED' section
  (COMMAND/HOST/EFFECT/ROLLBACK/EVIDENCE/PRECONDITION per envelope) or the literal
  'APPROVAL REQUIRED: none'. Report FAILED commands verbatim, never silently.
")

For simple single-command operations, execute directly:

EXECUTE using Bash tool:

. "${CLAUDE_PLUGIN_ROOT}/skills/deploy/scripts/lib/deploy-common.sh"
ght 60 ssh -o ConnectTimeout=10 -o BatchMode=yes SERVERNAME "COMMAND" && echo "OK" || { rc=$?; echo "FAILED $(ght_reason "$rc")" >&2; exit "$rc"; }

Step 5: Docker Auth (if needed)

If task involves Docker registry operations, read references/docker-auth-flow.md for auth patterns.

A registry token is never model-visible. Do NOT collect it with AskUserQuestion, do not put it in a command line, do not echo it, do not write it into a file you generate. Ask the user to prepare ONE of these OUTSIDE the conversation, then read it only inside Bash:

Source User prepares (outside the transcript) Skill reads it as
file (recommended) write the token to ~/.config/brewtools/ghcr.token, then chmod 600 that file < ~/.config/brewtools/ghcr.token
env var read -rs GHCR_TOKEN && export GHCR_TOKEN in the shell that launched Claude Code, before launching it "$GHCR_TOKEN" piped to stdin

The file path works immediately. The env var only works if the export happened BEFORE this session started: every Bash tool call spawns a fresh profile-initialised shell, so an export typed in another terminal — or in this one after launch — is invisible. If $GHCR_TOKEN is unset, say so and offer the file path or a relaunch; !=ask the user to paste the token.

Log in with stdin only — the value never appears in argv, never in ps, never in shell history:

EXECUTE using Bash tool:

printf '%s' "$GHCR_TOKEN" | ssh SERVERNAME "docker login ghcr.io -u USERNAME --password-stdin" >/dev/null 2>&1 && echo "OK login ghcr.io" || { rc=$?; echo "FAILED login ghcr.io" >&2; exit "$rc"; }

File variant:

ssh SERVERNAME "docker login ghcr.io -u USERNAME --password-stdin" < ~/.config/brewtools/ghcr.token >/dev/null 2>&1 && echo "OK login ghcr.io" || { rc=$?; echo "FAILED login ghcr.io" >&2; exit "$rc"; }

Report only OK login / FAILED login. If the variable is unset, say so and stop — never fall back to asking for the value in chat. Never run env, set, or cat on the token file.

Phase 6: Session Report

Field Value
Server SERVERNAME (HOST)
Mode [detected mode]
Actions [list of actions performed]
Changes [list of changes made on server]
Status success / partial / failed

After execution: if new info discovered, update CLAUDE.local.md; if server state changed significantly, update ssh-admin agent.

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" update "SERVERNAME" ...

Mode: update-agent

Re-discover all configured servers and refresh the ssh-admin agent.

Step 1: List Servers

EXECUTE using Bash tool:

bash "${CLAUDE_SKILL_DIR}/scripts/claude-local-ops.sh" list

Step 2: Re-discover Each Server

EXECUTE using Bash tool:

SSH_DISCOVER_TIMEOUT=30 bash "${CLAUDE_SKILL_DIR}/scripts/server-discover.sh" "USER@HOST" PORT && echo "OK discovery" || { rc=$?; echo "FAILED discovery rc=$rc" >&2; [ "$rc" -eq 124 ] || exit "$rc"; }

Same exit-code table as Phase 3. Never wrap in timeout.

Step 3: Update Config & Agent

Update CLAUDE.local.md with fresh data for each server. Regenerate .claude/agents/ssh-admin.md from template with updated inventory. Re-resolve {PLUGIN_VERSION} and {LAST_UPDATED} exactly as in Install Step 3 -- a regeneration is a new write, so the stamp is refreshed, never carried over. Report what changed since last update.

Output Format

# SSH [MODE]

## Detection

| Field | Value |
|-------|-------|
| Arguments | `$ARGUMENTS` |
| Mode | `[detected mode]` |

## Environment

| Component | Status |
|-----------|--------|
| SSH keys | [types found] |
| ssh-agent | [running/stopped] |
| SSH config | [exists/missing] |
| Servers configured | [N] |

## Server: [NAME]

| Property | Value |
|----------|-------|
| Host | [IP/hostname] |
| OS | [distribution] |
| Docker | [version] |
| Status | [connected/failed] |

## Actions Taken

- [action 1]
- [action 2]

## Status

[success / partial / failed]