Command line (macscp-cli)
macscp-cli is a small command-line companion. It ships inside the app bundle, so installing macSCP installs it too:
/Applications/macSCP.app/Contents/MacOS/macscp-cli ls prod:/var/wwwThe CLI works only with sessions already saved by the app — there is no way to pass a host and password directly on the command line. Point it at one with name:/path.
Installing the shortcut
Section titled “Installing the shortcut”For everyday use, put it on your PATH once. The easiest way is Settings → Command Line → Install: it creates a shortcut at ~/.local/bin/macscp-cli and tells you afterwards whether that shortcut still points at the app you are running — handy after moving the app or installing a new version, when an old shortcut would silently keep starting the previous copy. Use Repair to point it at the current one.
~/.local/bin has to be part of your shell’s PATH for the command to be found. macSCP does not edit your shell configuration; if the folder is not listed yet, add it yourself:
export PATH="$HOME/.local/bin:$PATH"If you would rather install system-wide, run this yourself — macSCP never asks for administrator rights, and the same command is shown ready to copy in Settings → Command Line:
sudo mkdir -p /usr/local/binsudo ln -sf /Applications/macSCP.app/Contents/MacOS/macscp-cli /usr/local/bin/macscp-cliBuilding from source also produces it, at .build/release/macscp-cli.
Shell completion
Section titled “Shell completion”Set up tab completion from Settings → Command Line → Shell Completion: pick your shell there and copy the line it shows. For zsh, that line is:
source <(macscp-cli --generate-completion-script zsh)Paste it into your shell’s startup file (~/.zshrc, ~/.bashrc, or ~/.config/fish/config.fish); bash and fish each have their own line shown there, in the form that shell expects. The line asks the installed tool for its current completion every time a shell starts, so an update never leaves a stale script behind, and it also offers the saved session names for the name: half. The completion reads the session list only — no secret, no connection.
Commands
Section titled “Commands”| Command | Effect |
|---|---|
sessions [--group <g>] [--kind <k>] [--name <text>] [--tag <t>] | List the saved sessions, optionally filtered. Reads no secret and opens no connection. |
sessions add <name> … / sessions edit <name> … / sessions rm <name> | Create, change, or remove a session. Writes the store, never a secret. |
ls <session>:<path> | List a remote directory. |
get <session>:<path> <local dir> | Download a remote file into a local directory (keeps its remote name). |
put <local file> <session>:<path> | Upload a local file into a remote directory (keeps its local name). |
rm <session>:<path> [--recursive] | Delete a remote file, or a whole directory with --recursive. |
mkdir <session>:<path> | Create a remote directory. |
diagnose <session> [--scope <s>] [--payload-mib <n>] | Measure the path to a saved session’s server, step by step. See Diagnose. |
diagnose --scope internet --speed-service <s> | Measure this Mac’s internet line against a speed test service. Takes no session and no --host. |
diagnose --host <host> [--port <n>] [--kind <k>] | The same measurement for a machine no session was saved for. |
tunnels list/add/edit/rm [--session <s>] | List, create, edit, or remove tunnel profiles over the app’s store. |
tunnels start <profile> --session <s> | Hold a forwarding open in the foreground, the way ssh -L does. See tunnels start output. |
get/put take --on-conflict fail|skip|overwrite for what to do when the destination already exists (fail is the default — nothing is overwritten unless asked). ls, sessions and diagnose take --json to emit one JSON object per line instead of columns, for scripting.
sessions add/edit/rm and tunnels … write to the same files the app itself reads, and the app picks up what the command line wrote the next time it becomes active — no restart needed. Both refuse an empty value, an empty name and a port outside 1–65535.
The file commands (ls, get, put, rm, mkdir) do not connect through a jump host: a session that uses one is refused with exit code 10. A forwarding cannot use a jump host or a login set either, so tunnels refuses such a session before it writes anything. diagnose does go through the jump host.
sessions never prints a user name or password that was typed into an S3 or WebDAV address; error lines leave them out, too.
Error lines stay in English — all command-line output does — but a named failure now prints one fixed sentence instead of an internal description, and it is the same sentence the diagnostic log records. A line you copy out of the terminal and a line you find in the log say the same thing, which makes them worth quoting together in a bug report.
sessions rm and forwardings
Section titled “sessions rm and forwardings”sessions rm also deletes the session’s forwardings. On a terminal it asks Delete session prod and 2 forwardings? [y/N] first; --yes skips the question. When the forwarding list cannot be read, it says so instead of counting zero — Delete session prod? Its forwardings could not be read and will be left in the forwarding list. [y/N] — deletes the session, and warns that the list was not changed.
Diagnose
Section titled “Diagnose”diagnose prints one row per step as it finishes — the name lookup, the connection attempt, the echo, the app’s own login and the route to the server. It never remembers a server’s identity on your behalf: a server this app has not been introduced to is reported as such, and no flag here changes that.
--scope | Runs |
|---|---|
complete (default) | Every step except throughput, and except internet. |
ping | The name lookup, the connection attempt and the echo. |
trace | The name lookup and the route to the server. |
dial | The name lookup and the app’s own login. |
contributions | The name lookup and the S3 and WebDAV checks. |
throughput | The name lookup and the throughput test: uploads a test file to the session’s start folder, downloads it, compares it and deletes it. Needs a saved session, not --host. |
internet | The internet speed test and nothing else — not even the name lookup. It measures no server, so it takes no session and no --host, and naming one is an error. |
--payload-mib <n> sets the throughput test file’s size, 1 to 256 MiB (default 8); it is accepted only with --scope throughput. The command line applies no bandwidth limit to the test.
--speed-service cloudflare|apple|off picks the service --scope internet measures against. It is required with that scope and refused with any other: the command line reads no settings file, so the service you chose in the app does not reach it, and the command asks you to name the service rather than picking one for you. --speed-service off contacts nobody.
The step downloads 10 MiB and uploads 1 MiB of generated data, gives each direction 45 seconds, and sends nothing about any session, login or host. A direction that is refused, too slow, or answered with a redirect to another server reads unavailable, and one this Mac was too busy to start reads notStarted; both leave the exit code at 0.
For a session behind a jump host, the rows start with the jump host (jump.resolve, jump.tcp, jump.icmp, jump.dial, jump.trace) and then reach the target through it (target.tcpViaJump, target.resolveOnJump, target.icmpFromJump, target.dialViaJump, target.traceFromJump). The jump host’s secret comes only from what the app saved for it; --password-command and MACSCP_PASSWORD apply to the target only. See Sessions behind a jump host.
The name lookup rows list each address with its reverse name and whether that name resolves back.
Stopping a throughput test
Section titled “Stopping a throughput test”Ctrl-C ends every other scope at once. During --scope throughput:
- The first Ctrl-C cancels the test, but still deletes the test file. The exit code follows the rows that are kept: 0, or 16 if one failed.
- A second Ctrl-C leaves at once, without waiting for the delete, prints
note: the run was left before its cleanup finished; the throughput test file … may remain in the session's start folderand exits 16.
Whenever the file may remain, a note: line on stderr names it, so you can delete it by hand.
diagnose --json
Section titled “diagnose --json”One JSON object per step, then one summary object:
| Key | In | Meaning |
|---|---|---|
id | step | The step’s id, for example resolve or jump.dial. |
outcome | step | ok, failed, timedOut, unavailable, skipped or notStarted. Added, never renamed: a script that switches on the five it knows keeps reading exactly the steps it read before. |
reason | step | Why the step did not pass; left out for ok and timedOut. |
durationMs | step | The step’s duration in milliseconds. |
detail | step | Free text, may be empty. |
hops | step | The route trace: a list of {hop, address, rtt, outcome}. |
names | step | The name lookup: a list of {address, name, check}. |
throughput | step | The throughput test: a list of {direction, bytes, duration, rate, limit}, all strings. |
internet | step | The internet speed test: a list of {direction, bytes, duration, rate}, all strings. |
completion | summary | complete, running or cancelled. |
endpoint | summary | {host, port} of the target, or null. null for --scope internet, which measures no server and names none. |
jump | summary | {host, port} of the jump host, or null. |
steps | summary | Every step object again. |
tunnels start output
Section titled “tunnels start output”tunnels start prints one line per state change: connecting, active, reconnecting attempt=N, needs confirmation, stopped, or failed. The active line adds port=P and connections=N. It also adds failed=N once connections through the running forwarding have failed, and degraded once three of them in a row have failed; the forwarding stays up, and both start again at zero with the next connection that opens. A failed line is followed by Error: <reason> on stderr, and the command exits 13 (or 10–12 for a login or host-key failure).
With --json, each line is one object with sorted keys:
| Key | In state | Meaning |
|---|---|---|
state | all | stopped, connecting, active, reconnecting, failed or needsConfirmation. |
connections | active | Open connections through the forwarding. |
port | active | The bound port, once known. |
failedConnections | active | Connections that failed while the forwarding stayed up; left out when 0. |
degraded | active | true once the last three connections in a row failed — the forwarding is up and carrying nothing; left out otherwise. |
attempt | reconnecting | The reconnect attempt’s number. |
reason | failed | Why the forwarding failed, in English. |
reasonIsGeneric | failed | true when no specific cause was known and reason is the general sentence for that kind of failure; false when reason names the specific cause. |
Secrets
Section titled “Secrets”A session’s password or key passphrase is looked up in this order, stopping at the first one that answers:
- An explicit
--password-command <cmd>— the command’s own stdout, trimmed. - An environment variable:
MACSCP_PASSWORDfor an SSH or WebDAV session,AWS_SECRET_ACCESS_KEYfor an S3 one. - The keychain, same as the app itself uses.
- For a session that logs in with a key the app’s key manager stores: the passphrase saved for that key.
A session authenticating through an SSH agent needs none of these — the agent supplies the key.
PEM-format private keys work here the same way as in the app; see PEM private keys. A PEM key the app cannot read ends with exit code 13 — convert it in the app or with ssh-keygen -p.
The keychain prompt
Section titled “The keychain prompt”The CLI reads the very same keychain items the app writes, and macOS asks your permission per item the first time a different program wants one. Choose Always Allow and the CLI is added to that item’s access list for good — every later run reads it without a prompt, which is what makes unattended use (a cron job, a CI runner) possible at all, provided the login keychain is unlocked. It is while you are logged in; a job on a logged-out machine, or one started over SSH, still cannot reach it. Deny or Allow Once leaves the prompt in place for the next run, which will simply fail where no one can answer it.
That standing permission is tied to the CLI’s code signature, so it holds for the signed copy shipped inside the app bundle; a macscp-cli you rebuilt yourself is a different signature and has to be confirmed again. For a machine that must never prompt, use --password-command or an environment variable instead and skip the keychain entirely.
One run can need more than one item. A session that logs in with a key the app’s key manager stores has two places its passphrase could sit — the session’s own entry and the key’s — and the CLI asks for the second only when the first holds nothing. Each entry is its own permission: answering for one grants nothing for the other. So what you grant depends on what the run actually read. If the session’s own entry answered, the key’s entry was never touched, and nothing was granted for it; should that first entry later stop answering — you cleared the passphrase field and saved, or the app moved the passphrase onto the key itself — the next run reaches a second entry for the first time and asks again. diagnose on a session behind a jump host can reach further still: the bastion’s login has entries of its own. Run the command by hand once after any such change, with someone there to answer, before leaving it to a schedule.
Host keys
Section titled “Host keys”The same first-connect trust-on-first-use rule as the app applies:
- An unknown host key is refused unless the CLI can either ask interactively or was given
--accept-new. - A host key that changed is always a hard stop, never something a flag can wave through.
--non-interactiverefuses to prompt even with a terminal attached; with no terminal at all (a cron job, a CI runner) the CLI refuses to prompt on its own.
Exit codes
Section titled “Exit codes”Stable and meant to be scripted against:
| Code | Meaning |
|---|---|
| 0 | Success. |
| 2 | Usage error found after the arguments were read (an unknown session, a directory where a file was expected, an empty destination, --host with a scope that needs a session). |
| 10 | Authentication failed. |
| 11 | Unknown host key, and nothing confirmed it (no terminal, or refused). |
| 12 | Host key changed since it was last trusted. |
| 13 | Connection failed. Also: tunnels start ended in failed; the forwarding list or the list of managed keys could not be read. |
| 14 | The remote path was not found, or access to it was denied. Also: the server did not start SFTP. |
| 15 | The destination already exists and --on-conflict fail (the default) was in effect. |
| 16 | diagnose finished, and at least one step failed or ran out of time. A skipped, unavailable or notStarted step does not set it: those measured nothing, and the last of them says only that this Mac was too busy to start the step. Also: a throughput test left with a second Ctrl-C. |
| 64 | The arguments themselves are wrong: an unknown flag, a missing argument, a value out of range. |