15 August 2026

What Claude Got Wrong: Automating SSH Access to Ruckus Unleashed APs

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:

  1. Check whether SSH key auth is supported — one web search, thirty seconds. Answer: no.
  2. Verify sshpass flag behaviour from the man page before suggesting it. sshpass -e reads SSHPASS, not a custom variable.
  3. Recognise that a restricted CLI shell requires different handling than a Linux shell. Paramiko is the right tool; sshpass piped through SSH is not.
  4. 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