A post handed over as a file
Until 1.6 a post came from a person at a terminal. The wizard asked its questions, opened an editor, asked what to do at the end. Nothing else could write one: not a script, not a cron job, not a phone.
./blog.sh add post.md
does the same work with the markdown handed over instead of typed. No editor, no questions, no dialog at the end. A bare filename is looked for in incoming/, so the file can arrive by the same upload as the photographs, and it is deleted once the post is written.
Where the wizard would ask, this refuses
A person can be asked. A script cannot, so every question becomes a refusal that writes nothing: a photo that has not finished uploading, an empty body, a file that is not text, a second filename where one was expected. The alternative — guessing — was measured on the old code, where a byte-order mark in front of the header was taken for body text: three invisible bytes from a Windows editor and the post arrived with no title and no tags, named after the words "title:" and "tags:", exit 0. 1.6 reads the header through them.
--json: one object, nothing else
With --json the answer is a single object on standard output. A post that was written carries slug, path, state, url, deploy and warnings, every key always present. A refusal carries ok: false, the reason as a code and one sentence.
And a refusal leaves with zero. That is deliberate, and it took a phone to learn it: iOS Shortcuts throws away the output of a remote command that failed, so every refusal a phone could meet came back as a bare status and nothing else — exactly when the reason was the whole point. The status answers the one question the object cannot: whether an answer arrived at all. A non-zero status means the engine is missing or the machine is not set up. Without --json nothing changes: prose on stderr and a non-zero status, as always.
Draft, unless the file says otherwise
The file stops at the draft. publish: yes in its front matter is the one thing a file may ask for that the wizard never could: to go straight out, the date settled, the announcement sent, the site rebuilt, the answer carrying the public address. A post sent from a train has no desk to come back to, so the choice is made when it is sent. Absent, or anything but yes/true/1, is a draft.
At a desk the same two decisions are flags: publish <slug> --yes answers the dialog in advance, and --no-announce puts the page up and sends nothing to Mastodon or Bluesky — toot can still send it by hand afterwards. One thing --yes will not decide for you: a post dated outside the recent window is published but not announced, and the run says so. Announcing is the one step that cannot be taken back.
--untrusted
Markdown that arrived over a wire gets one more rule: a picture reference may name only a bare filename. Without it,  reads the file into the post's media — right at a desk, where whoever typed it has the file anyway, and a door the moment the markdown comes from somewhere else.
None of this adds an API, a token or a listening port. A file is written somewhere the engine can see it, and a command runs. That is the whole integration surface, and it is the same one a person has.

Comments