Run the CLI on this machine
The candidate is a single Node.js program. It reads one configuration file, keeps its state under one state-root directory, and talks to platform APIs directly through an egress policy built from each provider's manifest. Nothing watches, schedules or runs in the background.
Build and install
Node.js 24.19 or newer, on macOS or Linux. The package is not published to a registry, so the supported path is to build it and pack the workspace:
git clone https://github.com/Syndroo/syndroo
cd syndroo
npm ci
npm run build
npm pack --workspace @syndroo/cli --pack-destination ./artifacts
npm install --global ./artifacts/syndroo-cli-0.7.0-rc.1.tgz
syndroo --version--version prints the candidate version and the Node.js version that ran it. --help prints the command list, and each command answers its own --help. Both work before any configuration exists and create nothing.
Configuration
There is one configuration source. Without --config the CLI reads $XDG_CONFIG_HOME/syndroo/config.json, falling back to $HOME/.config/syndroo/config.json. A missing default file is fine for the commands that resolve no provider; a missing file named by --config is an error. The path given to --config must be absolute.
{
"version": 1,
"stateRoot": "state",
"providers": {
"ghost": { "path": "vendor/ghost-provider" }
}
}version must be 1. stateRoot and each providers.<id>.path resolve relative to the directory of the configuration file, so changing the working directory cannot change which state or which plugin a run uses. Unknown keys are refused, and a key that looks like a token, secret, password or API key is refused by name rather than ignored. Configuration has the full contract.
Local state
The state root defaults to $XDG_STATE_HOME/syndroo/runtime-v1, falling back to $HOME/.local/state/syndroo/runtime-v1. It carries a single format marker, and the runtime refuses a root whose marker is missing or names another format. Earlier Syndroo versions used a different root; this runtime never probes, migrates or deletes it.
Reads never create the root and never repair it. A read-only command on a machine that has never connected anything reports the state as uninitialized instead of writing a directory into place.
Connect one provider
One credential source per run: --from-env reads the SYNDROO_CREDENTIALS environment variable, and --credential-file reads a private JSON file. Both are parsed as strict JSON and validated against the provider's own credential schema before anything is stored.
export SYNDROO_CREDENTIALS='{"identifier":"you.bsky.social","password":"APP_PASSWORD_PLACEHOLDER"}'
syndroo connect bluesky --from-env --label work --defaultAdding --label names the connection, and --default makes it the target a document gets when its target names no connection. A provider can hold several connections: connect again with --connection to refresh an existing one, use --update to change a label or the default flag, and --disconnect to remove the local connection. Disconnecting never revokes a platform token.
A machine caller can drive the same command with a whole request document instead of flags: --input takes a file, and - reads standard input. Every connection step is recorded, so replaying the same request returns the stored answer rather than connecting twice.
Prepare, then execute
A publication is frozen before it is sent. Preparing validates the content and the targets and returns a preview plus a single-use approval token; executing consumes that token.
{
"type": "prepare",
"content": { "text": "One document, chosen targets." },
"targets": [
{ "provider": "bluesky" },
{ "provider": "devto", "options": { "title": "Hello", "body_markdown": "One document." } }
]
}syndroo publish --input post.json --json
syndroo publish --input - < execute.json --jsonOn a terminal the prepare shows the frozen preview and asks for a yes or no; declining exits 5 and sends nothing. In --json the command never prompts and returns the prepared result, so a caller decides. Executing is only possible from standard input, and the token is minted and consumed by the same process family.
--data takes the same request document inline, which is convenient for a one-off but leaves the content in the process arguments. --dry-run parses and previews without writing state, resolving credentials or making a network request. --request-id gives the call a stable identity so a retry of the same logical intent is recognised.
Read status
Reads never mutate and never repair, and they never contact a platform. With no selector the overview lists provider availability, the connection count and the recent operations.
syndroo status --json
syndroo status --connections --json
syndroo status --operation <operationId> --json
syndroo status --operations --limit 20 --json--provider asks about one provider and may also filter the connection list. --limit and --cursor belong to --operations only, and an opaque cursor is passed back unchanged to fetch the next page.
Retry
Retrying is a mode of the same publish command, not a fourth command: --retry names the earlier operation and --to names the connection to retry, repeatable once per target. Only targets whose failure is known not to have been applied are eligible. A delivery whose outcome is unknown is never eligible: the platform may already hold the post, and the CLI will not find out by sending it again.
syndroo publish --retry <operationId> --to <connectionId> --jsonExit codes
An exit code says what the local call established, not whether a platform accepted the post. The exit code table lists all seven values, including the two that matter most: 4 means a write may have reached the provider with no known result, and 6 means a finished execution is known not to be fully successful.