scp-next is an SCP-style CLI and library for secure SSH file transfers. It uses SFTP through
ssh2-sftp-client rather than implementing SCP or SFTP directly.
Read the documentation, generate CLI snippets with the interactive examples, or browse the API documentation.
CLI:
npm install --global scp-next
Library:
npm install scp-next
Upload a directory:
scp-next upload ./dist /var/www/example \
--host your-host \
--username your-username \
--password your-password \
--recursive
Download a file:
scp-next download /var/log/example.log ./logs/example.log \
--host your-host \
--username your-username \
--password your-password
Output:
Downloading: /var/log/example.log 1.0 MB / 1.0 MB (100%)
Password arguments are convenient, but they may be exposed through shell history and process listings.
Prefer the SCP_NEXT_PASSWORD environment variable in shared or production environments. For key authentication, use a protected private-key file.
Use the library:
import { upload } from "scp-next";
await upload({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
password: process.env.SCP_NEXT_PASSWORD,
localPath: "./dist",
remotePath: "/var/www/example",
recursive: true,
overwrite: true
});
scp-next upload <source> <destination> [options]
scp-next download <source> <destination> [options]
scp-next run <job> [source] [destination] [options]
CLI commands use <source> <destination>. Programmatic upload and download APIs use
localPath and remotePath.
| Operation | Source | Destination |
|---|---|---|
| Upload | Local | Remote |
| Download | Remote | Local |
Destination paths follow familiar cp/scp behavior. If the destination exists as a directory or
ends with a path separator, scp-next places the source inside that directory using the source
basename. Missing destination directories are created by default.
Preview a recursive deploy before connecting:
scp-next upload ./dist /var/www/example \
--host your-host \
--username your-username \
--password your-password \
--recursive \
--dry-run
Deploy with overwrite and verbose diagnostics:
scp-next upload ./dist /var/www/example \
--host your-host \
--username your-username \
--password your-password \
--recursive \
--overwrite \
--verbose
Run commands after a successful upload:
scp-next upload ./dist /var/www/example \
--host your-host \
--username your-username \
--recursive \
--post-upload-command "cd /var/www/example && npm install --omit=dev" \
--post-upload-command "pm2 reload example"
--post-upload-command is repeatable and upload-only. Commands run sequentially on the
remote server with the SSH user's permissions. The first non-zero exit code stops the sequence
and causes the CLI to exit with a nonzero status. A missing exit status is also treated as failure.
Use an encrypted private key:
scp-next upload ./dist /var/www/example \
--host your-host \
--username your-username \
--private-key-file ~/.ssh/id_ed25519 \
--passphrase your-passphrase \
--recursive
Download a remote directory recursively:
scp-next download /var/log/example ./logs/example \
--host your-host \
--username your-username \
--password your-password \
--recursive
Download and overwrite an existing local file:
scp-next download /var/log/example.log ./logs/example.log \
--host your-host \
--username your-username \
--password your-password \
--overwrite
Run a configured download job:
scp-next run download-logs --config ./scp-next.config.json
| Option | Description |
|---|---|
--host <host> |
SSH server host. |
--port <port> |
SSH server port. Defaults to 22. |
--username <username> |
SSH username. |
--password <password> |
SSH password. |
--private-key <privateKey> |
Private-key content. Redacted from logs and errors. |
--private-key-file <privateKeyFile> |
Private-key file path. Supports ~ expansion. |
--passphrase <passphrase> |
Passphrase for an encrypted private key. |
--config <path> |
Explicit configuration file path. |
--profile <name> |
Named server profile from the configuration file. |
--recursive |
Transfer directories recursively. |
--overwrite |
Allow replacing existing destination files. |
--create-directories |
Create missing destination directories. Enabled by default. |
--no-create-directories |
Disable automatic destination directory creation. |
--dry-run |
Resolve and validate the operation without connecting or transferring. |
--post-upload-command <command> |
Run a remote command after a successful upload. Repeatable. |
--timeout <milliseconds> |
SSH connection ready timeout in milliseconds. |
--verbose |
Print non-sensitive diagnostic details. |
--quiet |
Disable progress and non-error output. |
--help |
Show command help. |
--version |
Show the package version. |
--timeout maps to the SSH readyTimeout, so it controls how long to wait for the connection handshake. It does not currently limit per-file or total transfer duration.
import { upload } from "scp-next";
await upload({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
password: process.env.SCP_NEXT_PASSWORD,
localPath: "./dist",
remotePath: "/var/www/example",
recursive: true,
overwrite: true,
postUploadCommands: [
"cd /var/www/example && npm install --omit=dev",
"pm2 reload example"
]
});
const { download } = require("scp-next");
async function main() {
await download({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
password: process.env.SCP_NEXT_PASSWORD,
remotePath: "/var/log/example.log",
localPath: "./logs/example.log"
});
}
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
import { createClient } from "scp-next";
const client = createClient({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
password: process.env.SCP_NEXT_PASSWORD
});
try {
await client.connect();
await client.upload("./dist", "/var/www/example", { recursive: true });
const result = await client.exec("pm2 reload example");
console.log(result.stdout);
await client.download("/var/log/example.log", "./logs/example.log");
} finally {
await client.close();
}
The CLI loads configuration files automatically. The library API receives options directly,
so load JSON in your application and pass the relevant server and transfer values.
import { readFile } from "node:fs/promises";
import { upload, type ScpNextConfig } from "scp-next";
const config = JSON.parse(
await readFile(new URL("./scp-next.config.json", import.meta.url), "utf8")
) as ScpNextConfig;
await upload({
...config.server,
...config.transfer,
localPath: "./dist",
remotePath: "/var/www/example"
});
| Field | Used by | Description |
|---|---|---|
host |
server | SSH server host. |
port |
server | SSH server port. Defaults to 22. |
username |
server | SSH username. |
password |
server | SSH password. |
privateKey |
server | Private-key content as a string or a Buffer. |
privateKeyFile |
server | Private-key file path. |
passphrase |
server | Passphrase for an encrypted private key. |
agent |
server | SSH agent socket path. |
hostFingerprint |
server | Expected server host-key SHA-256 fingerprint. |
knownHostsFile |
server | Known-hosts file for host verification. |
localPath |
upload/download | Local source for upload or local destination for download. |
remotePath |
upload/download | Remote destination for upload or remote source for download. |
recursive |
transfer | Transfer directories recursively. Defaults to false. |
overwrite |
transfer | Allow replacing existing files. |
createDirectories |
transfer | Create missing destination directories. Defaults to true. |
dryRun |
transfer | Validate and plan without modifying local or remote files. |
timeout |
server/transfer | SSH connection ready timeout in milliseconds. |
postUploadCommands |
upload | Remote command strings run sequentially after success. |
onProgress |
transfer | Progress callback for file and directory transfers. |
client.exec(command, options) returns ExecResult with stdout, stderr, exitCode,
and an optional signal. Its optional timeout is command-specific; failOnStderr can treat
stderr output as failure even when the exit code is zero. maxBuffer limits combined captured
output and defaults to 10 MiB. A timeout closes the SSH channel; whether the remote process is
terminated depends on the SSH server.
Use scp-next.config.json in the current directory. scp-next also auto-detects these rc-style
filenames: .scp-nextrc and .scp-nextrc.json.
scp-next.config.json
Use --config for an explicit path:
scp-next upload ./dist /var/www/example --config ./deploy/scp-next.json
Use --profile with a configuration file:
scp-next upload ./dist /var/www/example \
--config ./scp-next.config.json \
--profile production
Example:
{
"server": {
"host": "your-host",
"port": 22,
"username": "your-username",
"password": "your-password"
},
"transfer": {
"recursive": true,
"overwrite": true,
"createDirectories": true,
"timeout": 30000
}
}
Do not commit configuration files containing real passwords. Prefer SCP_NEXT_PASSWORD or another protected secret source for shared repositories and deployment environments.
| Key | Description |
|---|---|
server |
SSH connection options such as host, port, and username. |
transfer |
Transfer defaults such as recursive, overwrite, and directories. |
defaultProfile |
Profile name used when --profile or SCP_NEXT_PROFILE is absent. |
profiles |
Named server connection profiles. |
jobs |
Reusable upload or download jobs for scp-next run <job>. |
Root-level server options such as host, username, privateKeyFile, knownHostsFile,
and hostFingerprint are also supported for simple configuration files, but server keeps connection values grouped and easier to read.
{
"defaultProfile": "production",
"profiles": {
"production": {
"host": "your-host-a",
"port": 22,
"username": "your-username-a",
"password": "your-password-a"
},
"staging": {
"host": "your-host-b",
"port": 22,
"username": "your-username-b",
"privateKeyFile": "~/.ssh/id_ed25519"
}
}
}
scp-next upload ./dist /var/www/example --profile production
Jobs use source and destination; the operation determines which path is local or remote.
{
"profiles": {
"production": {
"host": "your-host",
"username": "your-username",
"password": "your-password"
}
},
"jobs": {
"deploy": {
"operation": "upload",
"profile": "production",
"source": "./dist",
"destination": "/var/www/example",
"recursive": true,
"overwrite": true,
"postUploadCommands": [
"cd /var/www/example && npm install --omit=dev",
"pm2 reload example"
]
},
"download-logs": {
"operation": "download",
"profile": "production",
"source": "/var/log/example",
"destination": "./logs",
"recursive": true
}
}
}
scp-next run deploy
scp-next run download-logs
scp-next run <job> [source] [destination] permits explicit path overrides.
SCP_NEXT_HOST
SCP_NEXT_PORT
SCP_NEXT_USERNAME
SCP_NEXT_PASSWORD
SCP_NEXT_PRIVATE_KEY
SCP_NEXT_PRIVATE_KEY_FILE
SCP_NEXT_PASSPHRASE
SCP_NEXT_TIMEOUT
SCP_NEXT_PROFILE
SSH_AUTH_SOCK
Bash:
export SCP_NEXT_HOST="your-host"
export SCP_NEXT_USERNAME="your-username"
export SCP_NEXT_PASSWORD="your-password"
scp-next upload ./dist /var/www/example --recursive
PowerShell:
$env:SCP_NEXT_HOST = "your-host"
$env:SCP_NEXT_USERNAME = "your-username"
$env:SCP_NEXT_PASSWORD = "your-password"
scp-next upload .\dist /var/www/example --recursive
Highest to lowest:
For run, job values are loaded first, then root configuration, selected profile, environment variables, explicit positional overrides, and CLI options.
Supported methods:
agent or SSH_AUTH_SOCKSSH agent CLI example:
ssh-add ~/.ssh/id_ed25519
export SCP_NEXT_HOST="your-host"
export SCP_NEXT_USERNAME="your-username"
scp-next upload ./dist /var/www/example --recursive
Library example:
import { upload } from "scp-next";
await upload({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
agent: process.env.SSH_AUTH_SOCK,
localPath: "./dist",
remotePath: "/var/www/example",
recursive: true
});
hostFingerprint compares the server host key SHA-256 fingerprint. knownHostsFile supports plain OpenSSH known_hosts entries for exact host names. When neither is supplied, scp-next reads ~/.ssh/known_hosts. Hashed host names and every OpenSSH marker variant are not currently parsed.
scp-next fails closed if it cannot establish a host verifier. Use hostFingerprint for CI or deployments where a known-hosts file is not available.
Interactive terminals display concise progress. Progress is disabled with --quiet or when stderr is not a TTY, which keeps CI logs readable. Library users can pass onProgress.
--dry-run resolves configuration and validates local paths and transfer direction without connecting to the remote server, transferring files, or executing post-upload commands. Planned post-upload commands are shown with known and recognizable secret values redacted.
Dry run: upload
Source: ./dist
Destination: your-username@your-host:/var/www/example
Recursive: yes
Overwrite: yes
Post-upload commands:
1. cd /var/www/example && npm install --omit=dev
2. pm2 reload example
Public typed errors:
ScpNextErrorConfigurationErrorValidationErrorAuthenticationErrorConnectionErrorTransferErrorRemoteCommandErrorFileSystemErrorHostVerificationErrorEach error includes a stable code, readable message, optional cause, and redacted non-sensitive context.
import { AuthenticationError, upload } from "scp-next";
try {
await upload({
host: process.env.SCP_NEXT_HOST,
username: process.env.SCP_NEXT_USERNAME,
password: process.env.SCP_NEXT_PASSWORD,
localPath: "./dist",
remotePath: "/var/www/example"
});
} catch (error) {
if (error instanceof AuthenticationError) {
console.error("SSH authentication failed.");
}
throw error;
}
Primary functions:
upload(options: UploadOptions): Promise<ExecResult[]>
download(options: DownloadOptions): Promise<void>
createClient(options: ScpServerOptions): ScpNextClient
copy(options: CopyOptions): Promise<void>
Upload and download APIs use explicit localPath and remotePath names.
The optional generic copy() API accepts typed local/remote endpoint objects.
ScpNextClient.exec(command, options) executes one command directly through SSH.
Post-upload execution is disabled unless postUploadCommands is present. Commands are sent
directly to the remote SSH server, never through a local shell, and never run for downloads or
dry runs. Do not put passwords, tokens, or other secrets directly in command strings; command
output may also contain application data. scp-next redacts known credentials and common secret
assignment forms from CLI logs and errors.
npm install
npm run typecheck
npm run lint
npm test
npm run build
npm run docs:links
npm run docs
npm pack --dry-run
prepublishOnly runs clean, typecheck, lint, tests, and build.
The published package includes dist, README, license, changelog, and handwritten guides. The CLI
entry is dist/cli/index.js and contains a Node.js shebang. npm run docs generates TypeDoc,
builds the website and examples with Webpack, assembles the GitHub Pages artifact under docs/,
and validates its SEO and PWA metadata. Do not edit or commit generated output directly.
Despite the package name, normal transfers use SFTP through ssh2-sftp-client, which is built on ssh2.
This avoids remote shell execution for regular transfers and gives better support for progress reporting and recursive directory traversal than shelling out to an SCP command.