Playbook: n8n SSH Worker Pattern — Claude Calls via SSH Execute

When to use this

Any n8n workflow that needs to call Claude (run an LLM prompt) must use the SSH Execute pattern. Do not use:

  • Direct api.anthropic.com HTTP Request nodes with ANTHROPIC_API_KEY
  • host.docker.internal:9999 (claude-proxy — removed 2026-06-24)

The SSH pattern uses the Claude Max subscription on the AI-Dev workers at zero marginal cost.


Available SSH credentials in n8n vault

Credential namen8n IDTarget hostUserKey source
vps-i1-root-sshEXatXoTcRtlCr3FM217.154.82.162 (vps-i1 / AI-Dev-IO1)rootradieu-root-2026-06-27 ed25519
bms-4-root-sshW8fOGECM0UwCagjd54.36.123.110 (bms-4 / AI-Dev-BMS4-1)rootsame key

Both use sshPrivateKey type with the radieu-root-2026-06-27 ed25519 key, whose public half is present in /root/.ssh/authorized_keys on both servers. On bms-4 the private half lives at /home/claude-runner/.ssh/vps_root_key (a byte-identical duplicate is also at /home/claude-runner/.ssh/id_bms1).

History (#4785, 2026-07-31): these credentials previously stored a dedicated claude-runner key at /home/claude-runner/.ssh/id_ed25519. That file was removed/renamed and never re-synced into the n8n vault, so every SSH-to-bms-4 workflow failed with All configured authentication methods failed. Both credentials were re-pointed at the radieu-root-2026-06-27 key (verified authorized on both hosts) and re-verified with a live test execution. If a dedicated non-root key is reintroduced later, add its public half to authorized_keys on both hosts before PATCHing the vault.

Capacity: vps-i1 → max 2–3 parallel claude sessions; bms-4 → max 4 parallel.


How to build the SSH Execute node

Node type

n8n-nodes-base.executeCommand with SSH enabled (SSH Execute node in the UI — same type as “Execute Command” but connected to an SSH credential).

Actually in n8n’s node registry this is n8n-nodes-base.ssh (the SSH node, not the Execute Command node).

Parameters

{
  "resource": "command",
  "operation": "execute",
  "command": "su -s /bin/bash claude-runner -c 'claude -p \"{{ $json.prompt }}\"'",
  "authentication": "privateKey"
}

Credentials block:

{
  "sshPrivateKey": {
    "id": "EXatXoTcRtlCr3FM",
    "name": "vps-i1-root-ssh"
  }
}

Getting the response

The SSH node returns:

{
  "stdout": "<claude's text response>",
  "stderr": "",
  "exitCode": 0
}

Reference the response in the next node as {{ $json.stdout }}.

Multi-line / complex prompts

For prompts containing quotes or newlines, write to a temp file:

cat > /tmp/n8n_prompt_{{ $execution.id }}.txt << 'PROMPT_EOF'
{{ $json.prompt }}
PROMPT_EOF
su -s /bin/bash claude-runner -c 'claude -p "$(cat /tmp/n8n_prompt_{{ $execution.id }}.txt)"'
rm -f /tmp/n8n_prompt_{{ $execution.id }}.txt

Use $execution.id in the filename to avoid collisions in parallel executions.

Timeout

Set the SSH node’s timeout to at least 120000 ms (2 minutes) — claude -p can take 30–90s for complex prompts.


Reference workflow

BrandPilot AI (id: SvcDlrMxBMN9aTUJ) — canonical implementation. Read its SSH Execute node before building a new one.


Rotating the SSH credentials in the n8n vault

The n8n vault credentials (vps-i1-root-ssh, bms-4-root-ssh) store the private key that authenticates as root on both target hosts. As of #4785 that is the radieu-root-2026-06-27 ed25519 key, readable on bms-4 at /home/claude-runner/.ssh/vps_root_key. They must be updated whenever that key rotates or whichever key the credentials point at is removed from authorized_keys.

The stored key must be one whose public half is in /root/.ssh/authorized_keys on the respective host. Confirm this before PATCHing — a mismatch produces All configured authentication methods failed at execution time with no clearer error (this is exactly what #4785 was).

When the SSH key rotates

After a new authorized key is in place on the target host(s), update the n8n vault. On a Linux worker (bms-4) the PATCH is done with curl + jq reading the private key directly from file (never echoed):

# bms-4 worker — read n8n API key from the deployed env, never print it
N8N_KEY=$(grep "^BMS4_N8N_API_KEY=" /opt/p24-infra/bms-4/.env | cut -d= -f2-)
N8N_HOST=$(grep "^BMS4_N8N_HOST=" /opt/p24-infra/bms-4/.env | cut -d= -f2- | tr -d '"' | sed 's:/*$::')
KEYFILE=/home/claude-runner/.ssh/vps_root_key   # radieu-root-2026-06-27
 
for pair in "W8fOGECM0UwCagjd:bms-4-root-ssh:54.36.123.110" "EXatXoTcRtlCr3FM:vps-i1-root-ssh:217.154.82.162"; do
  id=${pair%%:*}; rest=${pair#*:}; name=${rest%%:*}; host=${rest#*:}
  jq -n --rawfile pk "$KEYFILE" --arg name "$name" --arg host "$host" \
    '{name:$name, type:"sshPrivateKey", data:{host:$host, port:22, username:"root", privateKey:$pk}}' \
  | curl -s -X PATCH "$N8N_HOST/api/v1/credentials/$id" \
      -H "X-N8N-API-KEY: $N8N_KEY" -H "Content-Type: application/json" \
      --data-binary @- -o /dev/null -w "$name PATCH HTTP %{http_code}\n"
done
unset N8N_KEY

The Windows/PowerShell equivalent (dev machine) is below:

# 1. Read the n8n API key
$n8nHost = "https://n8n.bms-4.infra.zintegrowana.online"
$n8nKey = (Get-Content C:\code_2026\p24-infra\.env.local | Where-Object { $_ -match '^BMS4_N8N_API_KEY=' }) -replace '^BMS4_N8N_API_KEY=', ''
 
# 2. Get the private key from bms-4 (via root access) — radieu-root-2026-06-27
$newPrivKey = ssh -i C:\Users\konar\.ssh\id_ed25519 root@54.36.123.110 "cat /home/claude-runner/.ssh/vps_root_key"
Write-Host "Key length: $($newPrivKey.Length) chars"  # Should be ~400+
 
# 3. PATCH vps-i1-root-ssh credential
$credId = "EXatXoTcRtlCr3FM"
$body = @{
    name = "vps-i1-root-ssh"
    type = "sshPrivateKey"
    data = @{
        host       = "217.154.82.162"
        port       = 22
        username   = "root"
        privateKey = $newPrivKey
    }
    allowedHttpRequestDomains = "all"
} | ConvertTo-Json -Depth 5
 
Invoke-RestMethod -Uri "$n8nHost/api/v1/credentials/$credId" -Method PATCH `
    -Headers @{ "X-N8N-API-KEY" = $n8nKey; "Content-Type" = "application/json" } `
    -Body $body
 
# 4. PATCH bms-4-root-ssh credential
$credId2 = "W8fOGECM0UwCagjd"
$body2 = @{
    name = "bms-4-root-ssh"
    type = "sshPrivateKey"
    data = @{
        host       = "54.36.123.110"
        port       = 22
        username   = "root"
        privateKey = $newPrivKey
    }
    allowedHttpRequestDomains = "all"
} | ConvertTo-Json -Depth 5
 
Invoke-RestMethod -Uri "$n8nHost/api/v1/credentials/$credId2" -Method PATCH `
    -Headers @{ "X-N8N-API-KEY" = $n8nKey; "Content-Type" = "application/json" } `
    -Body $body2
 
Write-Host "Both SSH credentials updated in n8n vault"
# 5. Verify with a real execution. NOTE: the n8n public API has NO execute endpoint —
#    POST /api/v1/workflows/<id>/execute (and /run) return HTTP 405. Do not rely on it.
#    In queue mode `docker exec bms-4-n8n-1 n8n execute --id <id>` also fails
#    ("Task Broker's port 5679 is already in use").
#    Working pattern (used in #4785): create a throwaway workflow with a Webhook trigger
#    (responseMode: lastNode) -> SSH node (authentication: privateKey) running `echo`,
#    activate it, then curl the production webhook and assert exitCode 0 + expected stdout:
#      curl -s "$N8N_HOST/webhook/<path>"  ->  {"exitCode":null,"code":0,"stdout":"..."}
#    Deactivate + DELETE the throwaway workflow afterwards.

No SOPS entry for the SSH private key

The private key the n8n vault points at lives only on the server (bms-4) — it is NOT in SOPS. As of #4785 that is the radieu-root-2026-06-27 key at /home/claude-runner/.ssh/vps_root_key. The public key must be present in /root/.ssh/authorized_keys on every target host:

# Public half of the key the credentials use (check on bms-4 — safe, public key only):
ssh-keygen -y -f /home/claude-runner/.ssh/vps_root_key
# Its comment/fingerprint should match an entry in authorized_keys on BOTH hosts:
ssh root@54.36.123.110  "grep radieu-root-2026-06-27 /root/.ssh/authorized_keys | wc -l"   # -> 1
ssh root@217.154.82.162 "grep radieu-root-2026-06-27 /root/.ssh/authorized_keys | wc -l"   # -> 1

Troubleshooting

SymptomCauseFix
Execution completes in <200ms, no outputCredential type mismatch (sshPassword vs sshPrivateKey)See n8n-ssh-credential-type-mismatch.md
Permission denied (publickey) / All configured authentication methods failedThe key stored in the vault credential is not in authorized_keys on the target (the #4785 failure mode)Confirm the vault key is authorized: ssh-keygen -y -f /home/claude-runner/.ssh/vps_root_key and check its comment is in /root/.ssh/authorized_keys on the target; re-PATCH the credential if it drifted
bash: claude: command not foundsu -s /bin/bash drop to claude-runner, but PATH lacks /usr/bin/claudeUse full path: su -s /bin/bash claude-runner -c '/usr/bin/claude -p "..."'
Timeout after 120sLong prompt + slow modelIncrease SSH node timeout; for very long prompts use --model haiku for speed
Authentication expiredClaude Max session expired on the workerFollow claude-oauth-reauth.md for the affected worker
Response contains shell noiseclaude -p emits warnings to stdoutAdd 2>/dev/null or parse just the last output block

Capacity limits

WorkerMax parallel claude sessionsNotes
AI-Dev-IO1 (vps-i1)2–36 vCPU, 8 GB RAM
AI-Dev-BMS4-1 (bms-4)48 vCPU, 32 GB RAM

If a workflow is high-frequency, distribute across both workers by alternating the SSH credential used.