Sheet H2 · Reference

CLI

Run Hammer from a terminal or a script.

On desktop, Hammer is also a command line program. Every operation a plugin can call is a hammer command too, with the operation's input fields as options and its output printed as JSON. Installed plugins can add commands of their own; see Commands.

hammer project list
hammer scene read --project Storm --id 12
hammer help

That makes the CLI handy in two ways while you write a plugin. You can try an operation from a terminal and see exactly what your plugin will get back, and you can script Hammer itself: back up projects, export them on a schedule, or reinstall your plugin each time you build it.

Installing

Open Hammer's Settings and find Command line. What it offers depends on how you installed Hammer:

Install What Settings does
Linux package, Flatpak, or AppImage Add hammer to PATH writes a small script to ~/.local/bin/hammer
Snap Nothing to do: the snap provides the command, and Settings shows its name
macOS Add hammer to PATH writes /usr/local/bin/hammer, asking for an administrator password
macOS, from the App Store Shows a command to run once in Terminal, since the app can't write outside its sandbox
Windows Add hammer to PATH writes hammer.cmd to %LOCALAPPDATA%\Hammer\bin and adds that folder to your PATH
Windows, from the Microsoft Store Nothing to do: the store installs hammer for you

Terminals that were already open won't find hammer until you restart them. If ~/.local/bin isn't on your PATH, Settings tells you.

Hammer marks the script it writes, and only ever replaces or removes its own. When you move or update Hammer, Settings offers to update the script so it runs the new copy.

Some programs, such as an MCP client, start commands without looking in your PATH. Show the full command gives you the complete command for those.

How a call runs

You don't need Hammer open to use the CLI. Either way, a command works on the same projects you see in the app.

  • When Hammer is open, the command is passed to the running app, which runs it against its live state. That includes unsaved edits, and any change shows up in the app straight away.
  • When Hammer is closed, the command starts Hammer without a window, runs, and stops it again. Nothing carries over from one command to the next.

Only one process may change your data at a time. A command that runs without a window waits up to three seconds for another such command to finish, then gives up with exit code 5.

While the app is open, it keeps a few operations to itself so they can't race its own work: account login, account logout, account status, sync run, sync status, project rename and project delete. Run them with the app closed, or do them in the app.

Reference

hammer [--dev] <command> [--option value]...
hammer help [command]
hammer <command> --help

Commands

An operation's command is its name with the dots turned into spaces: scene.read is hammer scene read, and scene.read.many is hammer scene read many. See Operations for every one, with its input and output.

The CLI also has three operations no plugin can call, for managing plugins:

Command Input What it does
hammer plugin list none The installed plugins: id, name, version, description, enabled, and what each may use (operations, http, userHostSetting)
hammer plugin install --path, an absolute path to a .hammerplugin file Installs or replaces a plugin, granting every permission its manifest asks for. A running app picks it up at once
hammer plugin uninstall --id Removes a plugin and forgets its cache and secrets. Its other settings are kept for a reinstall

Installing and uninstalling both need --confirm, because installing grants permissions without asking.

Commands that plugins add sit alongside these, and hammer help lists them with the rest.

Options

Each input field is an option, named in kebab case: the field sceneId is --scene-id.

  • Values follow the option, as --name Storm or --name=Storm. A value that starts with -- has to use the = form.
  • Booleans can be given alone to mean true: --archived is the same as --archived true. Turn one off with --archived false.
  • Numbers must be numbers: --id 12.
  • Lists repeat the option: --tags magic --tags plot.
  • Objects take JSON.
  • Optional fields you leave out get their default.

These options work with every operation:

Option Meaning
--help Print the command's options instead of running it
--json <object> Give the whole input as one JSON object, instead of options. Any other input options are ignored
--confirm Required by operations that can't be undone, such as scene delete and plugin install. Without it the command refuses to run
--in <file> For an operation that takes a file, such as project import or entry image set: read it from <file>, or from standard input with -
--out <file> For an operation that returns a file, such as project export: write it to <file>, and print the rest of the output as JSON. With -, the file's bytes go to standard output instead

--dev, given before the command, uses Hammer's development data, as --dev does for the app. You'll only need it when running a development build of Hammer.

Reading standard input

The fields that hold text, such as a scene's markdown or a note's content, can come from standard input instead of an option. Leave the option out, and Hammer reads standard input to its end, dropping one trailing newline:

hammer scene write --project Storm --id 12 --mode live < chapter-one.md

Only one field can come from standard input in a call. --help shows which fields can, as or stdin.

Passwords are never taken as options. account login reads the password from the HAMMER_PASSWORD environment variable, or prompts for it without echoing when you run it at a terminal, or reads it from standard input otherwise.

Output

A command prints its output as pretty-printed JSON on standard output, and nothing else. If it fails, it prints a one-line message to standard error instead. sync run is the exception: it prints its result even when some projects didn't sync, and its exit code says how it went.

Exit codes

Code Meaning
0 Success
1 Failed: something went wrong that wasn't the caller's mistake. sync run also exits with 1 when anything failed to sync
2 sync run only: a project has a conflict, left for you to resolve in the app
3 Not logged in, or the sync server no longer accepts your login
4 Not found: no such project, scene, entry, or plugin
5 Busy: another Hammer command is using your data, a project is syncing, the app keeps this operation to itself, or the app is open but not answering yet
64 Usage: an unknown command or option, a bad value, a missing --confirm, or input the operation rejects

Code 5 is worth retrying after a moment; the others aren't.

Help

hammer help lists every command with a one-line description. hammer help scene read and hammer scene read --help both show one command's options:

hammer scene read: A scene's markdown, including unsaved edits, with its outline, notes, and tags.

  --project <string> (required)
  --id <integer> (required)

  --json <object>  The whole input as JSON, instead of options

For the full schemas, hammer ops list prints every operation with JSON schemas for its input and output, the same ones your plugin sees.

Examples

Projects

# Every project with its word count and when it was last edited.
hammer project list

# Create a project from a Markdown manuscript, one scene per ## heading.
hammer project import --name Storm --format markdown --markdown-split h2 --in storm.md

# Export it as a Word document.
hammer project export --project Storm --format docx --out storm.docx

# The formats you can export to, including those your plugins add.
hammer export formats

Scenes

# The scenes and groups in story order, with their ids and word counts.
hammer scene tree --project Storm

hammer scene read --project Storm --id 12

# Replace the text, keeping the old text as a draft.
hammer scene write --project Storm --id 12 --mode live < scene-12.md

# Or save it as a draft and leave the scene alone.
hammer scene write --project Storm --id 12 --mode draft --draft-name "Second pass" < scene-12.md

hammer scene append --project Storm --id 12 --markdown "The storm broke at dawn."

hammer scene create --project Storm --name "The lighthouse" --parent-id 3

Notes, ideas, and the encyclopedia

echo "Check the tide tables for chapter 4." | hammer note create --project Storm --tags research

hammer idea create --title "A lighthouse keeper who can't swim" --content "She took the job anyway."

hammer entry create --project Storm --name "Mara Vell" --type person --aliases Mara < mara.md
hammer entry image set --project Storm --id 7 --extension png --in mara.png

hammer search --project Storm --query "lighthouse #draft"

Account and sync

These run only while the app is closed.

HAMMER_PASSWORD="$(pass show hammer)" hammer account login --url sync.example.com --email me@example.com
hammer sync status
hammer sync run

Plugins

hammer plugin list
hammer plugin install --path "$PWD/build/wordfreq.hammerplugin" --confirm
hammer plugin uninstall --id wordfreq --confirm

Recipes

These are Bash scripts for macOS and Linux. Most use jq to pick values out of the JSON.

Reinstall your plugin on every build

Build your plugin, install it, and try it, in one step. If Hammer is open, the new version replaces the old one at once.

#!/usr/bin/env bash
# Builds the plugin and installs it into Hammer. Run it from the plugin's folder.
set -euo pipefail

./gradlew hammerPlugin    # ./build.sh in the Rust and C templates
package=$(ls -t "$PWD"/build/*.hammerplugin | head -n 1)
hammer plugin install --path "$package" --confirm | jq -r '"Installed \(.name) \(.version)"'

plugin install needs an absolute path, which is why the script uses $PWD.

Retry when Hammer is busy

Exit code 5 means Hammer couldn't run the command right now. This function retries a few times before giving up, and returns any other exit code at once.

# Runs hammer, retrying while another Hammer command or a sync is using your data.
hammer_retry() {
    local attempt code
    for attempt in 1 2 3 4 5; do
        hammer "$@"
        code=$?
        if [ "$code" -ne 5 ]; then
            return "$code"
        fi
        sleep 2
    done
    return "$code"
}

hammer_retry backup create --project Storm

Log your word counts each day

Run this from cron once a day to build up a CSV of every project's word count.

#!/usr/bin/env bash
# Appends today's word count for each project to ~/word-counts.csv.
set -euo pipefail

hammer project list |
    jq -r --arg day "$(date +%F)" '.projects[] | [$day, .name, (.wordCount // 0)] | @csv' \
    >> "$HOME/word-counts.csv"
0 23 * * * $HOME/bin/log-word-counts

Back up every project

#!/usr/bin/env bash
# Backs up every project, and reports any that failed.
set -uo pipefail

failed=0
while IFS= read -r project; do
    if hammer backup create --project "$project" > /dev/null; then
        echo "Backed up $project"
    else
        echo "Could not back up $project" >&2
        failed=1
    fi
done < <(hammer project list | jq -r '.projects[].name')

exit "$failed"

A command that reads standard input would also read the loop's list of projects. backup create doesn't, but if you put a command that does inside a loop like this one, give it its input explicitly, or < /dev/null.

Export everything, every night

Exports each project in a format into a folder named for the day.

#!/usr/bin/env bash
# Usage: export-all <format> [folder]. Run 'hammer export formats' for the formats.
set -euo pipefail

format=${1:?Give a format, such as docx or epub}
folder="${2:-$HOME/Exports}/$(date +%F)"
mkdir -p "$folder"

while IFS= read -r project; do
    file=$(hammer project export --project "$project" --format "$format" --out "$folder/$project.tmp" | jq -r .fileName)
    mv "$folder/$project.tmp" "$folder/$file"
    echo "$folder/$file"
done < <(hammer project list | jq -r '.projects[].name')

The export's output gives the file name Hammer would use, so the script writes to a temporary name and renames it.

Keep a manuscript in git

scene read many reads scenes in story order, a page at a time, so you can turn a project into a single Markdown file without one call per scene. Commit it each night and git gives you a history of every change.

#!/usr/bin/env bash
# Usage: snapshot <project> <repo>. Writes the project as Markdown and commits any change.
set -euo pipefail

project=$1
repo=$2
out="$repo/$project.md"
: > "$out"

after=""
while true; do
    page=$(hammer scene read many --project "$project" ${after:+--after "$after"})
    jq -r '.scenes[] | "## \(.name)\n\n\(.markdown)\n"' <<< "$page" >> "$out"
    after=$(jq -r '.next // empty' <<< "$page")
    if [ -z "$after" ]; then
        break
    fi
done

git -C "$repo" add "$project.md"
git -C "$repo" diff --cached --quiet || git -C "$repo" commit -q -m "$project on $(date +%F)"

Sync from a server or a cron job

sync run uses its exit code to say how it went, so a script can tell a conflict from a failure. It only runs while the app is closed, which suits a headless machine.

#!/usr/bin/env bash
# Syncs everything, and says what needs attention.
set -uo pipefail

result=$(hammer sync run)
case $? in
    0) echo "Synced" ;;
    2) echo "Needs resolving in Hammer:" $(jq -r '.projects[] | select(.outcome == "needs_resolution") | .name' <<< "$result") ;;
    3) echo "Not logged in. Run: hammer account login" >&2; exit 3 ;;
    5) echo "Hammer is open or busy; it syncs on its own while open." ;;
    *) echo "Sync failed:" >&2; jq -r '.log[]?' <<< "$result" >&2; exit 1 ;;
esac

By default a project with a conflict is left alone for you to resolve in the app. --on-conflict local keeps this device's version instead, and --on-conflict server takes the server's.

Capture ideas from anywhere

Bind this to a keyboard shortcut to turn whatever is on your clipboard into a story idea.

#!/usr/bin/env bash
# Saves the clipboard as a story idea. Uses pbpaste on macOS, wl-paste or xclip on Linux.
set -euo pipefail

if command -v pbpaste > /dev/null; then
    paste=pbpaste
elif command -v wl-paste > /dev/null; then
    paste=wl-paste
else
    paste="xclip -selection clipboard -o"
fi

$paste | hammer idea create --tags inbox | jq -r '"Saved idea \(.id)"'

Find scenes by tag

tag find returns every tagged thing in a project. Filter it to scenes, then read each one.

#!/usr/bin/env bash
# Usage: tagged-scenes <project> <tag>. Prints each scene tagged <tag>, with its word count.
set -euo pipefail

project=$1
tag=$2

hammer tag find --project "$project" --tag "$tag" |
    jq -r '.entities[] | select(.kind == "scene") | .id' |
    while IFS= read -r id; do
        hammer scene read --project "$project" --id "$id" < /dev/null |
            jq -r '"\(.id)\t\(.wordCount)\t\(.name)"'
    done

Words written this week

# GNU date; on macOS, use: date -v-6d +%F
hammer stats activity --project Storm --from "$(date -d '6 days ago' +%F)" |
    jq -r '.days[] | "\(.date)  \(.words)"'

Explore the operations

ops list describes every operation, so jq can answer questions about them.

# The operations that can't be undone.
hammer ops list | jq -r '.operations[] | select(.access == "destructive") | .name'

# Every option an operation takes, with its type.
hammer ops list | jq -r '.operations[] | select(.name == "scene.write") | .input.properties | to_entries[] | "\(.key): \(.value.type // "see schema")"'