Not sending the wrong thing
Rehearse writes before they happen, catch bad arguments before they leave the machine, and make destructive actions impossible to run by accident.
A wrong git push is annoying. A wrong connection request is a message from you to
a real person that you cannot take back. Automation on your own LinkedIn account
deserves a different standard of care than automation on your own files, and this
page is about the four things the CLI does to meet it.
Rehearse the write
--dry-run resolves everything — credentials, the account id, path parameters, the
query string, the request body, the final URL — and then stops. Nothing is sent.
$ connectsafely send-connection-request \
--profile-id ada-lovelace \
--custom-message "Loved your talk on analytical engines." \
--dry-run
{
"dryRun": true,
"command": "send-connection-request",
"method": "POST",
"path": "/linkedin/connect",
"query": { "accountId": "acc_1" },
"body": {
"profileId": "ada-lovelace",
"customMessage": "Loved your talk on analytical engines.",
"accountId": "acc_1"
},
"accountId": "acc_1",
"url": "https://api.connectsafely.ai/linkedin/connect?accountId=acc_1"
}This is worth building into your habits, for three reasons.
It is free. No request is made, so nothing counts against your weekly connection allowance or daily action caps. You can rehearse the same call twenty times.
It shows the account. accountId in the output is the account that would
actually act. If you manage several, this is the cheapest way to be certain a script
is not about to post from the wrong one.
It works on destructive commands without confirmation. You never have to arm a dangerous command in order to see what it would do.
The API key never appears in the output. You can paste a dry-run result into a ticket or a chat thread safely.
Catch bad arguments before they leave
Everything the API specification declares is checked locally first — required fields, enum values, numeric ranges, string lengths:
$ connectsafely get-latest-posts --profile-id ada --count 500
{
"code": "above_maximum",
"error": "--count must be <= 20 (got 500)",
"exitCode": 3,
"remediation": "Lower the value to at most 20. To read more rows, page through with the cursor or start parameter instead of raising the limit."
}No request was made. The remediation names the actual fix — page, do not inflate — rather than restating the constraint.
Missing values are reported together, so one correction fixes them all:
$ connectsafely withdraw-invitation
{
"code": "missing_required_flag",
"error": "withdraw-invitation is missing required flag: --profile-id",
"remediation": "Add --profile-id, then re-run. `connectsafely schema withdraw-invitation` lists every parameter with its type."
}Identifiers get extra scrutiny
A value that lands in a URL path is the one place a wrong argument silently addresses a different resource rather than failing. Those are checked hardest:
| Rejected | Why |
|---|---|
https://linkedin.com/in/ada | A URL where an id belongs — the single most common mistake |
../../admin | Path traversal |
a%2Fb | An encoded path separator |
| Control characters | Malformed or injected input |
| Empty | Would silently address the collection instead of the item |
Legitimate LinkedIn identifiers pass untouched, including URNs with colons, commas
and parentheses — urn:li:msg_conversation:(urn:li:fsd_profile:ABC,2-xyz==) is
fine.
Unknown keys are forwarded, not dropped
There is one deliberate exception to strictness. If you pass a key through --json
that the specification does not declare, the CLI forwards it untouched.
This looks lax and is the opposite. The API owns the parameter list, and it rejects
an unknown filter by name with a 400. If the CLI stripped the key instead, that
same call would return 200 with results the filter never touched — a search that
looks filtered and is not. A loud rejection beats a quiet wrong answer.
Undeclared flags, by contrast, are always rejected: --json is an explicit
"I know what I'm sending", a misspelled flag is not.
Destructive commands cannot run by accident
A small set of commands remove or retract something a person can see — currently
withdraw-invitation and conversations-delete-message. Without a terminal, they
refuse:
$ connectsafely withdraw-invitation --profile-id ada
{
"code": "confirmation_required",
"error": "withdraw-invitation removes something and was run without a terminal",
"exitCode": 4,
"remediation": "Re-run the exact same command with --yes once you have confirmed the intent. Use --dry-run first to see precisely what would be affected."
}The CLI refuses rather than prompts. A prompt in a script, a CI job or an agent
sandbox either hangs forever waiting for input that will never arrive, or is answered
by whatever byte happens to be on stdin. Neither is a confirmation. Exit code 4
is one, because it forces the decision back to whoever wrote the command.
At an interactive terminal these commands run normally — you are already the confirmation.
To find out what falls into this category:
connectsafely commands --kind write # everything that changes your account
connectsafely schema withdraw-invitation | jq '.destructive'Retries never duplicate an action
A failed write is not automatically retried. The CLI cannot know whether a request that timed out actually landed on LinkedIn, and a second connection request to the same person is a real-world mistake, not a wasted API call.
Writes are retried in exactly two situations:
- The server said it did nothing — a
429or a503. There is no action to duplicate. - You supplied
--idempotency-key— the API returns the first result for a repeated key, so the retry is safe by construction.
connectsafely send-connection-request \
--profile-id ada-lovelace \
--idempotency-key "invite-ada-2026-08-28"Use a key that is stable for the intent, not the attempt. A key derived from the profile and the campaign means a re-run of the same script does not send twice.
Reads are retried freely — there is nothing to duplicate.
Putting it together
The pattern worth adopting, for a person and an agent alike:
# 1. what does this command take?
connectsafely schema send-connection-request
# 2. what exactly would it send?
connectsafely send-connection-request --json @invite.json --dry-run
# 3. send it, once, safely
connectsafely send-connection-request --json @invite.json \
--idempotency-key "invite-ada-2026-08-28"Related
- When things go wrong — exit codes and error envelopes in full
- Sending at a human rhythm — the other half of protecting an account
Cutting the cost of every answer
Why the CLI costs an AI agent a fraction of what a tool server does — discovery on demand, and response pruning that happens before anything is read.
Sending at a human rhythm
Run a list of LinkedIn actions with randomised spacing between writes — because accounts get flagged on rhythm as much as on volume.
