Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
monotykamary avatar

Localterm

  • 20 installs
  • 11 repo stars
  • Updated August 4, 2026
  • monotykamary/localterm

Helps with ai & agent building tasks.

About

localterm is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • localterm
  • AI & Agent Building
  • AI-coding skill

Localterm by the numbers

  • 20 all-time installs (skills.sh)
  • +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #10,442 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/monotykamary/localterm --skill localterm

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs20
repo stars11
Last updatedAugust 4, 2026
Repositorymonotykamary/localterm

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

localterm API

localterm (https://github.com/monotykamary/localterm) is a local daemon that serves terminals as browser tabs. It exposes an unauthenticated, loopback-only HTTP API you can call with curl. The flagship resource is automations: server-managed cron jobs that, when due, open a new browser tab in a chosen directory and type the command into a fresh shell. The tab stays open after the command finishes so the user sees that it ran and whether it succeeded — never append exit to a command.

Connect

The daemon writes its state to ~/.localterm/:

PORT=$(cat ~/.localterm/server.port 2>/dev/null || echo 3417)
BASE="http://127.0.0.1:$PORT/api"
curl -s "$BASE/health"   # → {"ok":true,"sessions":N}

If the health check fails, the daemon isn't running. Ask the user to start it (or run it yourself if authorized):

npx @monotykamary/localterm@latest start

Requests must come from the same machine; Host must be loopback (using 127.0.0.1 with curl satisfies this).

The user-facing browser URL (what localterm status prints as url:) is resolved across three surfaces, best-first: tailnet (https://<node>.ts.net, when localterm install ran the Tailscale step), local (https://localterm.localhost, when the portless proxy service is up on :443), or loopback (http://localterm.localhost:<port>, always works via RFC 6761). The API calls above use the loopback raw form directly — don't depend on which surface the browser happens to use.

Automations

An automation is {name, trigger, cwd, command, enabled, limit, closeOnFinish}:

  • trigger — what makes the automation run, a tagged union on kind:
  • {kind:"schedule", schedule} — time-based (the common case).
  • {kind:"watch", recursive, filter?} — fires when the automation's cwd

changes, observed via native filesystem events (no polling). recursive (default true) watches the whole subtree. Optional filter is a glob pattern matched against the basename of the changed file (e.g. "*.mov" only fires on .mov files; "*.{mov,avi}" matches multiple extensions). When filter is omitted or empty, any change triggers the automation. Events from non-matching files are dropped before the debounce — the command never runs, and the run limit is unaffected. After a watch-triggered run finishes, a 1-second grace period suppresses new events so the command's own side effects (e.g. deleting the source file after conversion) don't retrigger the automation. A burst of changes is debounced into a single run, and no new run starts while a previous one is still in-flight. Watch triggers have no cron/nextRunAt (both null).

  • {kind:"event", events: [...]} — fires when a localterm session emits any

of the named events whose cwd matches the automation's cwd (or is inside it). This is session-scoped — the automation only triggers when you are _in_ localterm and the event occurs, not from background filesystem noise. A burst of events is debounced into a single run and no new run starts while a previous one is still in-flight. Event triggers have no cron/nextRunAt (both null).

Git events are detected by watching the repository's .git directory. The operation-level events are best-effort guesses based on which ref namespace moved and which git internal files were present during the change.

eventfires when
git-head-change.git/HEAD changes (checkout, reset, merge into detached HEAD)
git-branch-changea local branch ref is created, deleted, or moves (commit, merge, pull, reset, worktree add)
git-tag-changea tag is created, updated, or deleted
git-remote-changeremote-tracking refs change (fetch, pull)
git-stash-changethe stash ref changes
git-commitan existing local branch ref advances with no merge/rebase/reset state detected
git-checkoutHEAD changes while no branch ref moves
git-resetHEAD or a branch ref moves and ORIG_HEAD appears
git-mergea branch ref moves while MERGE_HEAD was present
git-rebasea branch ref moves while a rebase directory was present
git-cherry-picka branch ref moves while CHERRY_PICK_HEAD was present
git-fetchonly remote-tracking refs changed

| git-stash | the stash ref changed | | git-tag | a tag ref changed | | notification | a command emits OSC 9 (printf '\e]9;message\a') | | | in a session whose cwd matches — use your own scripts as event sources | | cwd | you cd into or out of the automation's directory | | foreground | the foreground process changes in a matching session (e.g. vim starts) | | exit | a shell session in a matching directory closes |

For "notify on push" workflows, use git-fetch or a custom notification event from your own hook — localterm cannot detect a bare git push from the local repository because push updates the remote, not local refs.

A schedule trigger's schedule is a structured schedule object (preferred) or a bare 5-field cron string — a tagged union on kind:

kindshapeexample meaning
hourly{kind:"hourly", minute}every hour at :minute
daily{kind:"daily", hour, minute}every day at 9am
timesOfDay{kind:"timesOfDay", times:[{hour,minute},…]}several fixed times a day (≤12)
weekdaysPreset`{kind:"weekdaysPreset", preset:"weekdays"\"weekends", hour, minute}`
weekly{kind:"weekly", daysOfWeek:[0–6], hour, minute}0=Sun … 6=Sat
monthly{kind:"monthly", daysOfMonth:[1–31], hour, minute}on the 1st and 15th
everyNMinutes{kind:"everyNMinutes", step}every N minutes
everyNHours{kind:"everyNHours", step, minute}every N hours on the clock
cron{kind:"cron", expression}the advanced escape hatch

Schedules are evaluated in the server's local timezone. The raw-cron escape hatch ({kind:"cron", expression}) and bare-string schedule support *, lists (1,15), ranges (9-17), steps (*/5, 9-17/2), month/weekday names (jan, mon-fri), and @hourly/@daily/@midnight/@weekly/@monthly/ @yearly. Vixie day semantics: if both day fields are restricted, either match fires. A bare-string schedule is recognized as a friendly preset where it maps cleanly, and kept as raw cron otherwise — losslessly either way.

  • cwd — absolute path; must exist and be a directory on the daemon's machine

(validated at create/update time).

  • command — typed into an interactive shell verbatim (shell syntax like &&

and pipes work). Max 4096 chars.

  • enabled — defaults to true. Disabled automations never fire.
  • limit{kind:"forever"} (default) or {kind:"count", max:N} = "stop after

N runs". When the limit is reached the automation finishes (a terminal lifecycle:"finished" state) and stops firing but stays listed with its history. Scheduled, watch, and event runs count toward the limit; manual /run never does.

  • closeOnFinish — defaults to false (the tab stays open). When true, the

run's browser tab is closed once the command finishes. Only honored for tabs opened via CDP (the background-tab path); on the open -g fallback it's a silent no-op since that tab has no closeable handle.

When a job fires (or is run manually), the server opens the daemon's resolved URL with a ?run=<id> query in the user's browser; the new tab claims the single-use run id, spawns a shell in cwd, and runs command. The shell stays open afterwards. For zsh/bash sessions the command's exit code is reported back and recorded in the automation's run history. The resolved URL is whichever surface localterm install configured (tailnet / local / loopback — see Connect); the 127.0.0.1:$PORT/?run=<id> raw form also works.

The run tab opens in the background (it does not steal focus). When a Chromium-based browser is running with remote debugging enabled, the server creates the tab behind the active one via the DevTools Protocol over a connection opened once at daemon start (so any remote-debugging prompt is cleared a single time, not per run); otherwise it falls back to the OS opener (macOS open -g, which keeps the browser from foregrounding). LOCALTERM_DISABLE_CDP_TABS=1 forces the fallback.

Endpoints

# List (each item adds computed nextRunAt epoch-ms (null when disabled/finished
# or a watch/event trigger), a derived `cron` string (null for watch/event), the capped
# `runs` history, and a back-compat `lastRun`)
curl -s "$BASE/automations"

# Create
curl -s -X POST "$BASE/automations" \
  -H 'content-type: application/json' \
  -d '{
    "name": "nightly build",
    "trigger": { "kind": "schedule", "schedule": { "kind": "daily", "hour": 2, "minute": 0 } },
    "cwd": "/Users/me/project",
    "command": "pnpm build && pnpm test",
    "enabled": true,
    "limit": { "kind": "forever" }
  }'
# → 201 {"automation":{"id":"…","cron":"0 2 * * *","nextRunAt":1765591200000,…}}

# Create a folder-watch automation (runs when cwd changes; no cron/nextRunAt)
curl -s -X POST "$BASE/automations" \
  -H 'content-type: application/json' \
  -d '{
    "name": "rebuild on change",
    "trigger": { "kind": "watch", "recursive": true },
    "cwd": "/Users/me/project",
    "command": "pnpm build",
    "limit": { "kind": "count", "max": 50 }
  }'
# → 201 {"automation":{"id":"…","cron":null,"nextRunAt":null,…}}

# Create a filtered folder-watch (only triggers on .mov files)
curl -s -X POST "$BASE/automations" \
  -H 'content-type: application/json' \
  -d '{
    "name": "autoconvert mov→mp4",
    "trigger": { "kind": "watch", "recursive": false, "filter": "*.mov" },
    "cwd": "/Users/me/Downloads",
    "command": "find /Users/me/Downloads -maxdepth 1 -iname *.mov -type f | while IFS= read -r f; do mp4=\"${f%.*}.mp4\"; if [ ! -f \"$mp4\" ]; then ffmpeg -y -i \"$f\" -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k \"$mp4\" && rm \"$f\"; else rm \"$f\"; fi; done",
    "enabled": true,
    "limit": { "kind": "forever" },
    "closeOnFinish": true
  }'
# → 201 {"automation":{"id":"…","trigger":{"kind":"watch","recursive":false,"filter":"*.mov"},…}}

# Create an event-triggered automation (fires on git ref changes in the directory)
curl -s -X POST "$BASE/automations" \
  -H 'content-type: application/json' \
  -d '{
    "name": "run tests after commit",
    "trigger": { "kind": "event", "events": ["git-commit"] },
    "cwd": "/Users/me/project",
    "command": "git log --oneline -1 HEAD",
    "enabled": true,
    "limit": { "kind": "forever" }
  }'
# → 201 {"automation":{"id":"…","cron":null,"nextRunAt":null,…}}
# Runs the command whenever a localterm session in /Users/me/project detects
# that git HEAD moved (commit, push, checkout, reset). No prompt-cycle
# noise — only real ref changes. For a webhook, pipe into curl inside the
# command — $DISCORD_WEBHOOK and other env vars are available.

# Create an event automation that reacts to a custom shell notification
curl -s -X POST "$BASE/automations" \
  -H 'content-type: application/json' \
  -d '{
    "name": "on deploy-complete signal",
    "trigger": { "kind": "event", "events": ["notification"] },
    "cwd": "/Users/me/project",
    "command": "echo \'Deploy cycle done\'",
    "enabled": true,
    "limit": { "kind": "forever" },
    "closeOnFinish": true
  }'
# → 201 {"automation":{"id":"…","trigger":{"kind":"event","events":["notification"]},…}}
# Fires when any command in this directory does: printf '\e]9;deploy-complete\a'

# Update any subset of fields (pass a `trigger` to change the schedule/watch/event)
curl -s -X PATCH "$BASE/automations/<id>" \
  -H 'content-type: application/json' \
  -d '{"limit": {"kind": "count", "max": 20}}'

# Delete
curl -s -X DELETE "$BASE/automations/<id>"

# Run immediately (opens the tab now; does not affect the schedule or the limit)
curl -s -X POST "$BASE/automations/<id>/run"
# → {"runId":"…"}

# Reset a finished automation (zeroes runCount, re-activates, re-enables).
# Optional body {"clearHistory": true} also empties the run history.
curl -s -X POST "$BASE/automations/<id>/reset"

Reading run state

Each automation carries runs (newest-first, capped at 50) of {runId, scheduledFor, startedAt, finishedAt, status, exitCode, trigger, countsTowardLimit}, plus runCount, lifecycle (active|finished), and a back-compat lastRun: {runId, at, status, exitCode} (= the newest run):

statusmeaning
launchedtab open requested, not yet claimed by a browser tab
runninga tab claimed the run and the command is executing
completedcommand finished with exit code 0
failedcommand finished with a non-zero exitCode
missedno tab claimed the run within 5 minutes (browser closed/headless)
skippedthe daemon was down at that scheduled minute — reconstructed at
startup from a downtime heartbeat (only the ~10 most-recent missed
occurrences per automation, so real runs aren't evicted); never re-run

completed/failed are only reported for zsh and bash login shells; other shells stay at running until the tab closes. The trigger field in each run record is "schedule", "manual", "watch", or "event". The automation-level lifecycle:"finished" means a count limit was reached — use POST …/reset to run it again.

Error responses

400 with {"error": "invalid_body" | "invalid_schedule" | "invalid_cwd" | "too_many_automations" | "automation_finished"}, or 404 {"error":"not_found"} for unknown ids. automation_finished is returned when a PATCH tries to re-enable a finished automation — reset it instead. On invalid_cwd, confirm the directory exists on the daemon's machine and retry with an absolute path.

Playbook

1. Health-check first; surface a clear "daemon not running" message if it fails. 2. Prefer one automation per task; reuse/update an existing automation with the same name instead of creating duplicates (list, then PATCH). 3. Prefer a structured schedule (e.g. {"kind":"daily","hour":9,"minute":0}) over raw cron so the user sees a friendly label; fall back to {"kind":"cron","expression":"…"} only for schedules the presets can't express. 4. After creating, echo back the human-readable schedule and the nextRunAt time so the user can confirm the intent. 5. To verify an automation end-to-end, trigger POST …/run (this does not count toward a limit) and poll the list until the newest runs[0].status / lastRun.status becomes completed (or failed — then read the tab). 6. Don't schedule destructive commands without explicit user confirmation. 7. For git-related workflows ("run tests after commit", "notify after merge"), use the granular git events such as {kind:"event", events:["git-commit"]}, {kind:"event", events:["git-merge"]}, or {kind:"event", events:["git-fetch"]}. Operation detection is best-effort; for guaranteed signals, use {kind:"event", events:["notification"]}and have your scripts emitOSC 9 as the signal. 8. When the command is too complex for a readable one-liner (loops, multi-step pipelines with temp files, heredocs, structured output payloads, etc.), write a shell script in the automation's cwd and set command to bash <name>.sh`. This keeps the automation JSON legible and the logic version-controlled:

   # Instead of inlining a 200-char pipeline, write e.g. push-watch.sh in cwd:
   curl -s -X POST "$BASE/automations" \
     -H 'content-type: application/json' \
     -d '{
       "name": "push watcher",
       "trigger": { "kind": "event", "events": ["git-fetch"] },
       "cwd": "/Users/me/open-source",
       "command": "bash push-watch.sh",
       "enabled": true
     }'

Other endpoints

curl -s "$BASE/health"                              # {"ok":true,"sessions":N}
curl -s "$BASE/git/diff-summary?cwd=/path/to/repo"  # {isRepo, files, additions, deletions, binaries}
curl -s "$BASE/git/diff?cwd=/path/to/repo"          # full per-file unified patches

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.