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.comHTTP Request nodes withANTHROPIC_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 name | n8n ID | Target host | User | Key source |
|---|---|---|---|---|
vps-i1-root-ssh | EXatXoTcRtlCr3FM | 217.154.82.162 (vps-i1 / AI-Dev-IO1) | root | radieu-root-2026-06-27 ed25519 |
bms-4-root-ssh | W8fOGECM0UwCagjd | 54.36.123.110 (bms-4 / AI-Dev-BMS4-1) | root | same 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-runnerkey 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 withAll configured authentication methods failed. Both credentials were re-pointed at theradieu-root-2026-06-27key (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 toauthorized_keyson 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 }}.txtUse $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_KEYThe 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" # -> 1Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Execution completes in <200ms, no output | Credential type mismatch (sshPassword vs sshPrivateKey) | See n8n-ssh-credential-type-mismatch.md |
Permission denied (publickey) / All configured authentication methods failed | The 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 found | su -s /bin/bash drop to claude-runner, but PATH lacks /usr/bin/claude | Use full path: su -s /bin/bash claude-runner -c '/usr/bin/claude -p "..."' |
| Timeout after 120s | Long prompt + slow model | Increase SSH node timeout; for very long prompts use --model haiku for speed |
Authentication expired | Claude Max session expired on the worker | Follow claude-oauth-reauth.md for the affected worker |
| Response contains shell noise | claude -p emits warnings to stdout | Add 2>/dev/null or parse just the last output block |
Capacity limits
| Worker | Max parallel claude sessions | Notes |
|---|---|---|
| AI-Dev-IO1 (vps-i1) | 2–3 | 6 vCPU, 8 GB RAM |
| AI-Dev-BMS4-1 (bms-4) | 4 | 8 vCPU, 32 GB RAM |
If a workflow is high-frequency, distribute across both workers by alternating the SSH credential used.