FTP Deploy
Incremental FTP/FTPS/SFTP deploy for VS Code, built around multiple servers as a
first-class concern.
- Profiles for every server you publish to, each with its own credentials,
paths and file selection — switch or right-click to deploy to any of them.
- Rules: a named pipeline of steps — run a build, back the server up, delete,
upload — that runs end to end with one click.
- Incremental by hash, not by timestamp, so only what genuinely changed goes
over the wire.
- Dry run everything first: the full plan, file by file, before anything moves.
- Atomic uploads, size verification, and a manifest written per file so an
interrupted run resumes cleanly.
- Passwords and passphrases live in the OS keychain, never in the config.
Getting started
Nothing needs to be hand-written.
- Open the FTP Deploy icon in the activity bar.
- Click + New profile — a form covers every setting: protocol, host, port,
credentials, key file, local and remote roots, include/exclude globs, and
the mirror-delete switch.
- Test connection inside the form, before saving.
- Save — the profile is written to
.vscode/ftp-deploy.json and the
password goes to the OS keychain.
- Right-click the profile → Dry Run, then Deploy.
Repeat step 2 for every server. Right-click any profile to deploy to it
directly, regardless of which one is active.
The config file stays readable and hand-editable — the editor writes through
jsonc-parser, so your comments and formatting survive.
Profiles
A profile is one server. Names must be unique, because the name is the key for
two separate things:
- The stored credential. Namespaced by scope and profile name, so
production in this project never reads production from another.
- The manifest section. Each profile gets its own block of upload state. Two
servers hold different state; a shared section would make a diff for one host
reflect uploads made to another.
Mark a profile development, staging or production and the environment
colours its badge everywhere it appears — the confirmation before a production
deploy is the one you want to actually read.
Commands resolve their target in this order:
right-click → active profile → defaultProfile → first profile.
Rules
A rule is a named pipeline: pick the files once, then list the steps to run
on them. Running the rule executes every step in order with no further
prompting — that is the whole point of it.
Add one from a profile → New rule. The editor has three parts: where it
deploys, which files it acts on, and what it does to them.
Choosing the files
One selection per rule, applied to whichever side each step reads from.
- Include files — the local folder as a tree, with the ticks showing what
the globs really match, evaluated by the same matcher the deploy uses.
- Exclude patterns — the raw glob lists, for anything a tree cannot express.
- Server files — the live remote tree, browsed on demand. Ticking here edits
the same lists. The bin icon deletes on the server immediately and is not
part of the rule.
Every list carries the same toolbar: filter by name, Expand all,
Collapse, Reload, Select all, Unselect all.
Ticking a folder writes folder/**; unticking one file adds just that path.
Content-hashed names are generalised for you — untick main.1b26a9b7.js and the
rule stores main.*.js, which still matches after the next build.
Steps
| Step |
What it does |
| Run a command |
A shell command — your build — with its own working folder. |
| Back up |
Timestamped copy of the server files about to be replaced, saved to your disk, with a MANIFEST.txt receipt. |
| Upload |
Only files that changed (manifest diff) or Every matched file, always (override). |
| Delete remote files |
Only what this run backed up, or everything the selection matches. |
| Delete local files |
Removes the matched files from your disk. |
| Mirror deletes |
Removes remote files this profile uploaded that no longer exist locally. |
Each step says what it will cost before you run it — an upload with no backup
step above it says so, and a delete paired with a backup says that the server
only loses files already saved to your disk.
Any step can narrow the rule's selection further under Files for this step:
a folder tree of exactly what that step will touch, where unticking affects that
step alone. "Back up everything, then delete all of it except web.config" is
two ticks.
Presets
One click each: Upload changed · Upload with override · Back up server ·
Back up then upload · Back up, delete, upload · Build, back up, delete, upload ·
Back up local then delete local · Upload + mirror deletes.
The rule that keeps "back up then delete" safe
Delete local files is rejected unless a back up or upload step comes
before it — enforced in the editor, again before the file is written, and again
when the config loads. Order matters: deleteLocal then backup is refused.
At run time it goes further. Each step records which files it verified landed
on the server (size-checked, manifest-recorded). deleteLocal deletes only
those files. A file whose upload failed is kept and logged:
kept src/ui/app.js — not confirmed on the server this run
So a half-failed transfer can never take your local copy with it.
Running
- Dry Run Rule — every step, what it would touch, destructive ones flagged.
- Run Rule — executes the pipeline, with live progress per file.
- Run All Rules — every enabled rule in order.
Rules that delete always confirm first, even inside Run All, and the
confirmation lists the actual steps. Disable a rule to keep it but skip it in
Run All.
Where profiles and rules are stored
By default they live in the folder that uses them, at .vscode/ftp-deploy.json —
committed with the project, reviewed with the project.
If you deploy the same handful of servers from a dozen different folders, keep
one set for the whole machine instead. Run FTP Deploy: Config Location (This
Folder or This Machine) and pick This machine:
|
This folder |
This machine |
| Profiles and rules |
.vscode/ftp-deploy.json |
~/.ftp-deploy/ftp-deploy.json |
| Upload state |
.vscode/.ftp-deploy-manifest.json |
~/.ftp-deploy/manifest.json |
| Passwords |
keychain, per folder |
keychain, once per machine |
| Active profile, run history |
per folder |
shared |
Switching offers to copy the current folder's config across, and leaves the
other file untouched — switching back loses nothing. Passwords are namespaced
per scope, so set them once after a first switch.
Rules stored for the machine should use absolute local paths: there is no
one folder for a relative path to resolve against, and they may run in a window
with no folder open at all.
How change detection works
Deploy compares local files against the manifest, not against the server.
FTP exposes no file hash, and its timestamps drift with server timezone and DST,
so remote metadata cannot reliably answer "did this change?". Instead, every
confirmed upload is recorded as sha256 + size + timestamp. The next run hashes
locally and uploads only what differs.
Commit .vscode/.ftp-deploy-manifest.json to git so every machine deploys the
same incremental set. Delete a profile's section — or run Reset Manifest —
to force a full re-upload.
Backups
A Back up step copies files into a timestamped folder on your disk —
2026-08-17_1430/ — before anything overwrites or deletes them. It can copy
either side: the server files about to be replaced, or your local files.
- The folder it writes into comes from the step, or from
localBackupRoot when
the step leaves it blank.
snapshotFolderFormat names the timestamped subfolder.
snapshotWriteReceipt adds a MANIFEST.txt listing every file's hash and
size, so a backup can be verified or restored without this extension.
Backups are always complete, never incremental, and are never written to the
server. Nothing prunes them — delete old folders yourself.
Safety
- Dry run first.
confirmBeforeDeploy (default on) shows the plan, then asks
for a modal confirmation.
- Atomic writes. Every file uploads to
<name>.part and is renamed into
place. A dropped connection leaves an orphaned .part, never a truncated
web.config serving 500s.
- Size verification after each transfer, before the manifest records it.
- Manifest written per file, not at the end — an interrupted run leaves a
manifest describing exactly what actually landed, and the next run resumes.
- Deletes are opt-in.
mirrorDeletes is false by default. Files gone
locally are reported as orphans and left alone.
- Excludes always beat includes, plus a built-in default exclude list
(
node_modules, .git, dist, .env, *.log…).
- Secrets live in the OS keychain, never in the config file.
Settings
FTP Deploy: Open Settings, or search ftpDeploy in the Settings UI.
Per-server details live in profiles; these are the behaviour knobs.
| Setting |
Default |
What it does |
configScope |
workspace |
workspace = this folder, global = ~/.ftp-deploy/ for the whole machine. |
configPath |
.vscode/ftp-deploy.json |
Where profiles live, in workspace scope. |
manifestPath |
.vscode/.ftp-deploy-manifest.json |
Where upload state lives. |
showStatusBar |
true |
Active profile in the status bar. |
confirmBeforeDeploy |
true |
Modal confirmation before transferring. |
openPlanInEditor |
false |
Also open the plan as a text document. |
verifySizeAfterUpload |
true |
Check remote byte count before recording. |
confirmDeletes |
true |
Separate second prompt for remote deletions. |
retries |
3 |
Per-file attempts; backoff 2s/4s/8s. |
commandTimeoutMs |
600000 |
How long a command step may run. |
listConcurrency |
4 |
Connections the rule editor lists server folders over. 1 if the host refuses extra sessions. |
connectTimeoutMs |
20000 |
Connection timeout. |
maxFileSizeMb |
0 |
Skip files over this size. 0 = no limit. |
useDefaultExcludes |
true |
Apply the global exclude list. |
defaultExcludes |
(11 globs) |
Excluded from every profile. Fully editable. |
followSymlinks |
false |
Follow links when scanning. |
localBackupRoot |
(empty) |
Default folder for back up steps that name none. |
snapshotFolderFormat |
YYYY-MM-DD_HHmm |
Name of the timestamped backup subfolder. |
snapshotWriteReceipt |
true |
Write MANIFEST.txt into each backup. |
Config reference
For hand-editing .vscode/ftp-deploy.json. It accepts // and /* */ comments.
| Key |
Required |
Notes |
name |
✅ |
Unique. Keys the credential and manifest section. |
protocol |
✅ |
sftp | ftp | ftps |
host / username |
✅ |
|
remoteRoot |
✅ |
Absolute remote path. |
port |
|
Defaults to 22 (sftp) or 21. |
privateKeyPath |
|
SFTP only. Passphrase goes in the keychain. |
localRoot |
|
Workspace-relative. Defaults to the workspace root. |
include / exclude |
|
Globs. Include builds the set, exclude subtracts. |
environment |
|
development | staging | production. Colours the badge. |
mirrorDeletes |
|
Default false. |
secureRejectUnauthorized |
|
ftps only. Leave true unless you know why. |
Known limits
- Uploads are sequential. Both FTP and SFTP clients drive a single channel,
so parallel calls on one connection corrupt each other. Browsing the server in
the rule editor already uses a pool of connections (
listConcurrency); the
same trick would work for uploads and has not been done yet.
- Depth is not parallelisable. A folder must be listed before its children
are known, so a deep chain of single folders is one round trip at a time
however many connections are open. Wide trees — a resources folder per
locale — are where a pool pays.
- No scheduling. An extension only runs while VS Code is open; there is no
background activation.
- No restore/download yet.
Building from source
npm install
npm run package # runs the tests, then builds ftp-deploy-<version>.vsix
code --install-extension ftp-deploy-1.0.0.vsix --force
Reload the window afterwards. F5 launches an Extension Development Host
instead, and npm run watch rebuilds on save.
src/
config/ types, settings, JSONC read + write, SecretStorage
core/ scanner (globs), hash, manifest, plan, rule engine
transport/ Transport interface, sftp + ftp impls, atomicPut, retry
commands/ one file per user-facing action
ui/ dashboard, profile and rule editors, deployment panel
core/ and transport/ are free of vscode imports on purpose — that is what
keeps a future CLI possible.
License
MIT