a hook is any executable file. when the timer hits zero (or you ship early), keystroke writes your post to a temp markdown file and runs the hook with it.
keystroke looks for hooks at $KEYSTROKE_HOOK, falling back to ./hook
relative to where the server was started. every file must exist and be
executable, otherwise the ui refuses to let you write.
./hook and hooks/local/ are gitignored — keep personal hooks there (or
anywhere outside the repo) and they will never be committed.
KEYSTROKE_HOOK takes colon-separated paths, run sequentially:
export KEYSTROKE_HOOK=./hooks/front-matter.sh:./hooks/publish.sheach hook must exit 0 before the next one runs; on failure the rest are skipped and the ui shows which hook broke, with the output of everything that ran. all hooks receive the same file path, so earlier hooks can edit the post in place for later ones — add front matter first, publish second.
--x- flags on the command line are forwarded to every hook as environment
variables, so you can set per-run values without exporting anything:
node src/server.js --x-blog-tags=tech
make dev ARGS="--x-blog-tags=tech"the name after --x- is uppercased with dashes turned into underscores and
prefixed with KEYSTROKE_, so --x-blog-tags=tech arrives as
KEYSTROKE_BLOG_TAGS=tech. the value can also be space-separated
(--x-blog-tags tech), and a bare flag (--x-blog-dry-run) is set to 1.
make consumes ---prefixed words itself, so route them through ARGS.
| input | meaning |
|---|---|
$1 |
absolute path to the markdown file |
KEYSTROKE_TITLE |
the title as typed, may be empty |
KEYSTROKE_SLUG |
lowercased, hyphenated title |
KEYSTROKE_DURATION_MINUTES |
the duration that was picked |
the file is named YYYY-MM-DD-<slug>.md and contains only the editor body,
no front matter. add your own in the hook if your blog needs it.
- exit 0: the post is considered published and the session is cleared
- any other exit code: the ui shows your stdout/stderr and the draft path
- stdout is shown in the ui on success too, so print something useful
- hooks are killed after 2 minutes by default; override with
KEYSTROKE_HOOK_TIMEOUT(milliseconds)
#!/bin/sh
set -e
blog=$HOME/projects/my-blog
post=posts/$(basename "$1")
branch=post/$KEYSTROKE_SLUG
cd "$blog"
git checkout main -q && git pull -q
git checkout -b "$branch" -q
{
echo "---"
echo "title: $KEYSTROKE_TITLE"
echo "date: $(date +%Y-%m-%d)"
echo "---"
cat "$1"
} > "$post"
git add "$post"
git commit -qm "post: $KEYSTROKE_SLUG"
git push -qu origin "$branch"
gh pr create --fill
gh pr merge --squash --delete-branch
echo "published $post"make demo starts the server with hooks/wordcount.sh, a bundled hook that
counts the words in your post and publishes nothing. it exists so you can
feel the timer before wiring up a real destination.
run it by hand before trusting it with a deadline:
echo "# test post" > /tmp/test.md
KEYSTROKE_TITLE="Test" KEYSTROKE_SLUG=test ./hook /tmp/test.md