Adding a New CLI Command and Hermes Skill (sls Worked Example)

tutorial

Post 2026-A-0138

A worked example based on the sls (see-latest-screenshot) command. Written for an LLM agent that needs to do something similar.


What We Built

A command sls (alias see-latest-screenshot) that: - In a terminal: runs hermes -z with a prompt containing the latest screenshot path - In a Hermes TUI session: the agent recognizes “sls” as a trigger phrase and runs the same logic directly (find latest file, copy to tmp, call vision_analyze)


Step 1: Investigate Before Building

Always check what already exists before writing anything.

which sls                          # is it already in PATH?
ls ~/av/bin/ | grep sls            # check the workspace bin dir
ls ~/.local/bin/ | grep sls        # check local bin
hermes skills list | grep <topic>  # is there a skill stub?

If a skill exists, load it:

skill_view(name="screenshot_tool")

A stub skill (no real steps, broken frontmatter) is worse than nothing – it creates false confidence. Check whether the skill actually works before trusting it.


Step 2: Understand the Environment

Before writing any script, collect these facts:

echo "SCREENSHOTDIR=${SCREENSHOTDIR:-not set}"
echo $PATH | tr : '\n' | grep av/bin   # is ~/av/bin in PATH?
which hermes                            # confirm hermes location

In this workspace: - SCREENSHOTDIR is set in the shell environment: /Users/crasch/av/ast/img/screenshots - ~/av/bin/ is in PATH (workspace convention) - hermes is at ~/.local/bin/hermes - hermes -z “prompt text” runs a one-shot (non-interactive) agent call


Step 3: Write the Shell Script

Put scripts in ~/av/bin/ (not ~/.local/bin/ directly). Symlink aliases there too.

Key decisions for this script:

  1. Respect SCREENSHOTDIR, fall back to ~/Pictures/Screenshots
  2. Use find + xargs + ls -t to get the newest file (handles filenames with spaces safely when combined with -print0 and xargs -0)
  3. Copy to a space-free /tmp path before passing to vision_analyze (see Pitfall #1)
  4. Use exec hermes -z “$PROMPT” to hand off without a subprocess layer

Final script at ~/av/bin/sls:

#!/usr/bin/env bash
SCREENSHOTDIR="${SCREENSHOTDIR:-$HOME/Pictures/Screenshots}"

LATEST=$(find "$SCREENSHOTDIR" -maxdepth 1 \
  \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) \
  -print0 2>/dev/null \
  | xargs -0 ls -t 2>/dev/null | head -1)

if [[ -z "$LATEST" ]]; then
  echo "sls: no screenshots found in $SCREENSHOTDIR" >&2
  exit 1
fi

TMPFILE="/tmp/sls_latest.png"
cp "$LATEST" "$TMPFILE"

QUESTION="${1:-Describe everything you see in this screenshot in detail.}"
echo "sls: loading $LATEST" >&2

PROMPT="Use vision_analyze to look at the image at this path: ${TMPFILE}
Question: ${QUESTION}"

exec hermes -z "$PROMPT"

Make it executable and create the alias:

chmod +x ~/av/bin/sls
ln -sf ~/av/bin/sls ~/av/bin/see-latest-screenshot

Verify syntax:

bash -n ~/av/bin/sls && echo "syntax OK"

Step 4: Test the Core Tool (vision_analyze) in Isolation

Before wiring everything together, prove the underlying tool works. Find a screenshot without spaces in the name and test directly:

vision_analyze(
  image_url="/Users/crasch/av/ast/img/screenshots/fb-post-verified.png",
  question="Describe what you see."
)

If that works, the tool is fine. If the only screenshots have spaces in their names, copy to /tmp first:

cp "/path/with spaces/file.png" /tmp/test.png
vision_analyze(image_url="/tmp/test.png", question="Describe what you see.")

Do NOT assume vision_analyze handles all path formats. Test first, then encode the workaround into the script and skill.


Step 5: Write a Real Skill (not a stub)

A skill must have: 1. Valid YAML frontmatter (no bare - as a value – it parses as a list) 2. Trigger conditions (what phrases should load this skill) 3. Numbered steps the agent can follow mechanically 4. A pitfalls section with known failure modes

Bad frontmatter that breaks skill_manage: homepage: - # WRONG – bare - is a YAML sequence marker

Good frontmatter: — name: screenshot_tool description: “One sentence summary.” tags: [tools, vision, screenshots] —

The skill body for screenshot_tool:

## Trigger conditions
- User says "sls", "see latest screenshot", "show latest screenshot"

## Steps
1. Run: find "${SCREENSHOTDIR:-$HOME/Pictures/Screenshots}" -maxdepth 1
         \( -name "*.png" -o -name "*.jpg" \) -print0 | xargs -0 ls -t | head -1
   Capture path as LATEST.
2. Copy: cp "$LATEST" /tmp/sls_latest.png
3. Call: vision_analyze(image_url="/tmp/sls_latest.png", question=<user question>)
4. Report what the image shows.

## Pitfalls
- vision_analyze BREAKS on paths with spaces. Always copy to /tmp first.
- vision_analyze rejects file:// URIs. Pass raw filesystem paths only.
- If SCREENSHOTDIR is wrong, check: echo $SCREENSHOTDIR

Write the skill with skill_manage(action=‘edit’, …) or action=‘create’ if new.


Pitfall Reference

Pitfall 1: vision_analyze breaks on paths with spaces

macOS generates screenshot names like: Screenshot 2026-06-07 at 1.29.59 PM.png

Passing this path directly to vision_analyze fails with: “Invalid image source. Provide an HTTP/HTTPS URL or a valid local file path.”

The fix is always to copy to a space-free path first: cp “$LATEST” /tmp/sls_latest.png vision_analyze(image_url=“/tmp/sls_latest.png”, …)

Also does NOT work: file:///path/to/file.png (file:// URI – rejected) /path/to/Screenshot%202026.png (URL-encoding – rejected)

Pitfall 2: Broken YAML frontmatter in skills

homepage: -    # breaks skill_manage with "sequence entries not allowed"

Use an empty string or omit the field entirely if you have no value.

Pitfall 3: Stub skills create false confidence

If a skill exists but is hollow, the next agent will load it, see instructions, and assume they work. Always verify a skill actually does what it says. If it is a stub, rewrite it completely with skill_manage(action=‘edit’).

Pitfall 4: sls is a shell command, not a TUI command

sls as a shell script calls hermes -z – this only makes sense at a terminal prompt. Inside a Hermes TUI session, the user types “sls” as a message and the agent picks it up via the skill’s trigger conditions. The agent then runs the find + cp + vision_analyze flow directly, without spawning a subprocess.


Verification Checklist

After completing the task, confirm all of these:


Want to stay in touch?

Support my work