A recent session set out to do something straightforward: give a Claude agent passwordless SSH access to three Ruckus R720 Unleashed access points on the home LAN. It should have taken twenty minutes. It did not. This post documents exactly what went wrong, why, and what the working solution eventually looked like.
The Goal
The setup: three Ruckus R720 APs running Unleashed firmware 200.15.6.212.27, on IPs 10.140.2.16, 10.140.2.17, 10.140.2.18. A Claude agent needs to SSH into them to query status and make configuration changes — without the human sharing a password in source code.
The natural first question: does Ruckus Unleashed support SSH public key authentication?
Failure #1: Presenting Guesses as Instructions
Rather than looking this up, Claude immediately proposed a CLI procedure:
Step 2 — Enter enable mode, then config:
enable config
Step 3 — Add the SSH public key:
admin ssh-key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."
This command does not exist on Ruckus Unleashed. The user dutifully typed it and got nothing. When pressed, Claude admitted it "wasn't confident" — but only after the user had already wasted time at the terminal.
The correct first step would have been to search the Ruckus documentation before suggesting anything. A five-second web search would have returned the answer: SSH public key authentication is not supported on Ruckus Unleashed in any firmware version, including 200.15.x.
Failure #2: Wrong sshpass Syntax
Once it was established that SSH keys weren't supported, Claude pivoted to sshpass — a reasonable choice for password automation. But then:
Store the password in an environment variable:
RUCKUS_PASS=yourpassword
Agent calls:
bash sshpass -e ssh -oHostKeyAlgorithms=+ssh-rsa admin@10.140.2.16
sshpass -e reads from a specific environment variable named SSHPASS — not an arbitrarily named one. From the actual man page:
-e The password is taken from the environment variable "SSHPASS".
The user caught this immediately. Claude had not checked the man page before writing the instruction.
Failure #3: Invalid SSH Option
In fixing the sshpass issue, Claude suggested suppressing SSH's LANG environment forwarding with:
ssh -oSendEnv=""
This is not valid syntax. OpenSSH rejected it with:
command-line line 0: no argument after keyword "sendenv"
The working fix — verified from the man page — is to unset LANG in the calling environment:
LANG= sshpass -f /path/to/passfile ssh ...
Failure #4: Assuming the Ruckus SSH Shell Behaves Like Linux
The core technical misunderstanding underlying several failed attempts: Claude assumed that passing a command as an SSH argument (the standard Unix way) would work:
sshpass -f .ruckus_pass ssh admin@10.140.2.16 "show sysinfo"
This fails because Ruckus Unleashed's SSH daemon (dropbear 2018.76) does not execute commands directly. It presents an interactive CLI session. The error was:
Invalid argument
Connection to 10.140.2.16 closed.
The "Invalid argument" was dropbear rejecting the LANG environment variable forwarded by OpenSSH, and the connection closed immediately because no PTY was allocated. A restricted shell CLI requires a PTY — -tt — and commands piped via stdin.
But -tt conflicts with sshpass's own internal PTY allocation, causing exit code 255. The correct solution is paramiko, Python's SSH library, which gives full programmatic control over PTY allocation and interactive session handling.
Failure #5: Not Knowing About the Double Authentication
Once the switch to paramiko was made, the script connected but immediately failed because the channel closed before commands could be sent. Debug output revealed the cause:
print(repr(drain(chan, 3)))
# '\r\nPlease login: '
Ruckus Unleashed presents its own CLI login prompt after SSH authentication. Even though paramiko had authenticated successfully at the SSH layer, the Ruckus software requires a second username and password exchange before the ruckus> prompt appears. This is not documented prominently and was only discovered by observing what the AP actually sent.
The correct login sequence:
read_until(chan, "Please login: ")
chan.send("admin\n")
read_until(chan, "Password: ", "password : ") # prompt varies by AP
chan.send(password + "\n")
read_until(chan, "ruckus> ", "rkscli: ") # master vs non-master
Failure #6: Not Knowing About Two Different CLI Types
The three APs don't all present the same shell. The Unleashed master (whichever AP currently holds that role) presents the full Unleashed CLI:
ruckus>
ruckus#
Non-master APs present a raw AP shell:
rkscli:
The commands are completely different. On the master:
ruckus# show sysinfo
On non-master APs:
rkscli: get version
This was only discovered when the script worked on .16 but timed out on .17 and .18, with debug output showing:
Warning: AP is in Unleashed-Managed mode
Current or latest Unleashed: 10.140.2.16 / 54:ec:2f:2b:f5:70
rkscli:
The Working Solution
After all of the above, the working implementation is a Python script using paramiko that handles both CLI types:
#!/usr/bin/env python3
"""
Run commands on a Ruckus Unleashed AP via SSH.
Usage: ruckus_ssh.py <ip> <command> [<command2> ...]
Password is read from /home/user/claude/.ruckus_pass
"""
import re
import sys
import time
import paramiko
PASS_FILE = "/home/user/claude/.ruckus_pass"
TIMEOUT = 15
ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\[[A-Za-z]")
def read_until(chan, *prompts, timeout=TIMEOUT):
buf = ""
deadline = time.time() + timeout
while time.time() < deadline:
if chan.recv_ready():
chunk = chan.recv(4096).decode("utf-8", errors="replace")
buf += chunk
if "--More--" in buf:
chan.send(" ")
buf = buf.replace("--More--", "")
if any(p in buf for p in prompts):
return buf
else:
time.sleep(0.05)
raise TimeoutError(f"Timed out waiting for {prompts!r}. Got: {buf!r}")
def run_commands(ip, commands):
with open(PASS_FILE) as f:
password = f.read().strip()
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username="admin", password=password,
look_for_keys=False, allow_agent=False)
chan = client.get_transport().open_session()
chan.get_pty()
chan.invoke_shell()
# Ruckus re-authenticates at CLI level after SSH auth
read_until(chan, "Please login: ", "login: ")
chan.send("admin\n")
read_until(chan, "Password: ", "password : ")
chan.send(password + "\n")
# Detect master (Unleashed CLI) vs non-master (rkscli)
banner = read_until(chan, "ruckus> ", "rkscli: ")
is_master = "ruckus> " in banner
if is_master:
chan.send("enable\n")
read_until(chan, "ruckus# ")
cmd_prompt = "ruckus# "
else:
cmd_prompt = "rkscli: "
output = ""
for cmd in commands:
chan.send(cmd + "\n")
result = read_until(chan, cmd_prompt)
lines = result.splitlines()
lines = [ANSI_ESCAPE.sub("", l) for l in lines]
lines = [l for l in lines if l.strip() not in (cmd.strip(), cmd_prompt.strip())]
output += "\n".join(lines).strip() + "\n"
try:
chan.send("exit\n")
time.sleep(0.3)
except OSError:
pass
client.close()
return output.strip()
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <ip> <command> [<command2> ...]", file=sys.stderr)
sys.exit(1)
print(run_commands(sys.argv[1], sys.argv[2:]))
The password file:
echo 'yourpassword' > /home/user/claude/.ruckus_pass
chmod 600 /home/user/claude/.ruckus_pass
Test against all three APs:
$ python3 ruckus_ssh.py 10.140.2.16 "show sysinfo" | grep -E "IP|Model|Version"
IP Address= 10.140.2.16
Model= r720
Version= 200.15.6.212 build 27
$ python3 ruckus_ssh.py 10.140.2.17 "get version"
Ruckus R720 Multimedia Hotzone Wireless AP
Version: 200.15.6.212.27
OK
$ python3 ruckus_ssh.py 10.140.2.18 "get version"
Ruckus R720 Multimedia Hotzone Wireless AP
Version: 200.15.6.212.27
OK
What Should Have Happened
The correct sequence, if Claude had followed its own stated principles:
- Check whether SSH key auth is supported — one web search, thirty seconds. Answer: no.
- Verify sshpass flag behaviour from the man page before suggesting it.
sshpass -ereadsSSHPASS, not a custom variable. - Recognise that a restricted CLI shell requires different handling than a Linux shell. Paramiko is the right tool; sshpass piped through SSH is not.
- Debug interactively with a minimal test script to observe what the AP actually sends before writing production code.
The actual sequence involved six separate wrong turns, each one caught by the user rather than by Claude checking its own work.
Summary
| Failure | What Claude Did | What It Should Have Done |
|---|---|---|
| SSH key support | Invented CLI commands | Searched Ruckus docs first |
| sshpass env var | Claimed -e reads any named var |
Checked the man page |
| SendEnv syntax | Used -oSendEnv="" |
Checked SSH man page |
| Shell behaviour | Treated Ruckus CLI like Linux shell | Recognised restricted shell needs PTY + paramiko |
| Double auth | Didn't know about it | Discovered via debug script before writing production code |
| Dual CLI types | Didn't know about it | Same — debug first |
The working solution is not complex. The path to it was far longer than it needed to be because of a repeated failure to verify before presenting information as fact.
No comments:
Post a Comment