haunted-term

View all files

Activity

Contributors

README

        .-.
       (o o)
       |\_/|
      /|   |\
     / |   | \

#   #  ###  #   # #   # ##### ##### ####        ##### ##### ####  #   #
#   # #   # #   # ##  #   #   #     #   #         #   #     #   # ## ##
##### ##### #   # # # #   #   ####  #   #  ###    #   ####  ####  # # #
#   # #   # #   # #  ##   #   #     #   #         #   #     #  #  #   #
#   # #   #  ###  #   #   #   ##### ####          #   ##### #   # #   #

  the shell remembers what you typed
  catalog lines are typed, never run

How it haunts you

haunted-term possesses the terminal while a shell or another program runs. It types a catalog command onto the screen, then leaves a lesson card. The open-source model in the loop is local Gemma (gemma3:1b through Ollama). Whispers can use your username and the last few commands you submitted. With --no-ai, or when Ollama is unreachable, those lines come from the local fallback instead.

Quick start

npx haunted-term demo
node bin/haunted-term.js demo

From a clone of this repo:

npm install
node bin/haunted-term.js demo
npm link
haunted-term demo

npm start runs node bin/haunted-term.js. A bare haunted-term prints help. It does not open a shell.

Requirements

  • Node.js 18 or newer. The package is ESM.

  • Optional: Ollama serving Gemma gemma3:1b at http://127.0.0.1:11434.

    ollama pull gemma3:1b
    ollama serve
    

    haunted-term doctor reports Node, whether Ollama answers GET /api/tags, whether gemma3:1b is listed, node-pty, and the Entire CLI. A model name without a tag is compared as :latest, so gemma3 does not count as gemma3:1b.

  • Optional: node-pty. If it is missing, or HAUNTED_NO_PTY=1 is set, wrap still runs by spawning your command with inherited stdio. Scares then go to stderr while the child looks idle. A remark about a command you just submitted exists only in the pty shell, because that fallback leaves the keyboard to the child.

Shell mode

haunted-term shell

Opens $SHELL, or bash when SHELL is unset. On Windows: COMSPEC, then SHELL, then powershell.exe. The lines you type and submit with Enter are kept (the last five, after secret redaction) so a whisper can say something like "Charan... why did you run npm test four times?"

Wrap mode

haunted-term -- cursor-agent
haunted-term -- claude
haunted-term -- codex
haunted-term -- npm run dev
haunted-term wrap echo hello

haunted-term -- <command> is wrap mode. The wrapped command line itself is remembered, plus lines you type into that program and submit. The same five-line ring applies. The wrapped process is the command haunted-term starts, apart from the allowlisted entire queries below. Scare text is written to your terminal. It is never typed into the child.

With node-pty, the session is a real pty: keystrokes are forwarded, resize is forwarded, and a scare uses the alternate screen (\x1b[?1049h) so the live session can be restored (\x1b[?1049l) before buffered output is flushed.

Idle haunting

Idle haunting needs the pty addon. In wrap and shell, a pty session that stays focused and untouched for 10 seconds gets a scare. While it stays idle and focused, the next one waits a random gap of about 0.8× to 1.4× that interval — about 8–14 seconds at the default. Any key breaks the wait and aborts a scare already on screen. Those keystrokes are delivered to the child after the terminal is restored. Without a pty, idle haunting does not run.

haunted-term shell --idle 20
haunted-term shell --no-idle

--idle <seconds> sets the untouched interval. Decimals >= 1 are accepted. 0 disables idle haunting only. --no-idle does the same. Random haunts still follow --frequency.

On a pty the terminal is asked to report focus (\x1b[?1004h). If it never answers, the session is treated as focused. Focus leaving the window cancels the wait. Focus coming back starts it over. In wrap mode, child output resets the wait when a scare is not running. It does not abort one.

Random haunting

haunted-term shell --frequency low
haunted-term -- cursor-agent --frequency high

--frequency is low, medium (the default), high, or a number from 0 to 1. The moment inside each window is random.

SettingGap between scares
low or 0.258–15 minutes
medium or 0.5 (default)3–6 minutes
high or 145–90 seconds
0never on the random timer (idle haunting can still run)
any other number from 0 to 1scaled between those windows

About 28% of wrap takeovers, when Entire is enabled, recall checkpoints instead of typing a catalog command. A scare waits while you are typing (about 2 seconds), while the trailing line looks like a password prompt, while pty echo is off, or while the child is already on the alternate screen. Without a pty, a random scare is printed on stderr only while the child looks idle.

Demo and scare types

haunted-term demo
haunted-term --demo
haunted-term demo --scare all
haunted-term demo --scare ghostTyping

demo and --demo play immediately. They do not wait for idle. --scare all plays each type once. --scare <type> plays one. Wrap and shell accept the same type names. all is demo only.

TypeWhat you see
ghostTypingA shell prompt, then a catalog command typed on screen, then a progress bar.
kernelPanicA kernel panic on Linux and macOS, or a blue screen (:(, stop code CRITICAL_PROCESS_DIED) on Windows.
deletionStreamDirectory names streaming past as removal lines. The names are read with readdir.
screenTearShifted blocks, inverse video, and a bell.
slowRedA whisper, then a countdown from 10 to 1, then a cleared screen.
fakeSudoThe ghost types sudo and then its own dots at a password prompt.

--os linux, --os mac, or --os windows chooses which catalog pool a scare may draw from. linux and mac also include posix rows. demo picks from that pool, so the next run can type a different command. HAUNTED_FAST=1 skips the typewriter delays. NO_COLOR or a stdout that is not a TTY turns color off.

Calm

haunted-term shell --calm
haunted-term -- cursor-agent --calm

--calm banishes haunting for wrap and shell: no random scare, no idle scare, and no remark on a submitted line. demo still plays the scare you asked for. HAUNTED_CALM=1 (true, yes, or on) does the same.

Learning

Every scare ends with a lesson card: command, severity, category, what it does, why it is dangerous, and a safer alternative.

haunted-term explain "rm -rf /"
haunted-term explain "fork bomb"
haunted-term grimoire --os windows --severity catastrophic

explain never runs the query.

  1. Exact. The query matches a catalog command or a tags entry after case and whitespace are folded. That row wins.
  2. Fuzzy. Otherwise the search scores command, id, tags, what, and category. A score above 0.75 is a catalog lesson, with up to three related spirits.
  3. Labeled Gemma. Anything else is sent to local Gemma and printed under ⚠ AI-GENERATED by gemma3:1b — may be wrong, verify before trusting. With --no-ai, or when Ollama is down, the model is missing, or the reply is empty, the line is the spirits are silent.

grimoire prints a table and a count footer. --severity is catastrophic, critical, high, or medium. --category is one of filesystem, disk, permissions, process, network, git, database, container, kubernetes, cloud, package, security, system, or history. --search is a fuzzy search over rows that already passed the other filters. --limit is a positive integer. --random prints one card. --json prints the same selection as JSON. --os for grimoire is linux, mac, windows, cross, or current (current follows this machine: darwin is mac, win32 is windows). An empty filter prints the grimoire has no such lesson.

--severity, --category, --search, --limit, and --random are refused outside grimoire. --json is refused outside explain and grimoire. explain needs a query and does not accept a command after --.

Entire

haunted-term haunt-entire

haunt-entire shows up to three real checkpoints, a lore line, and an on-screen rewind, then stops on Entire remembers everything you did. Checkpoints can be rewound with entire rewind.

Feature commits in this history came from Cursor CLI (cursor-agent) prompts. Those sessions are recorded as Entire sessions and checkpoints. .entire/settings.json sets commit_linking to always, and those feature commits include Entire-Checkpoint trailers. The repo is at https://entire.com/et/haunted-term/haunted-term.

Entire's review runners — trail-review, trail-security, trail-risk, trail-drift, trail-confidence, and trail-summary, defined under .entire/runners — are set up to read a trail. The Entire Brain plugin (entire brain) indexes past sessions into repo memories. AGENTS.md and .entire/agent-guide.md feed those memories back to the next agent.

haunt-entire does not run a general Entire command. It calls execFile only with these frozen argument lists, JSON first and the text form when stdout is not JSON:

  • entire checkpoint list --json then entire checkpoint list
  • entire session list --json then entire session list

Checkpoint and session fields pass through stripTerminalControls before display. The lines entire checkpoint list and entire session list at the end of the scare are printed hints. They are not executed. With no binary and no checkpoints the scare says no memories here... yet. run entire enable. --no-entire forces that path. --entire allows recall during wrap, which is already the default when an entire binary is on PATH.

entire checkpoint list
entire why src/possession.js:315
entire search "osc 52"
entire trail show 2

Safety

haunted-term never executes anything. Catalog commands and scare text are display only. Nothing in data/grimoire.json is passed to spawn, exec, execFile, or a pty write.

  • Deletion stream only reads directory names (fs.readdir / readdirSync, with file types). It never creates, modifies, renames, or deletes a file, and it never calls a write, unlink, rm, rename, or chmod API.
  • Ghost sudo types its own dots. It never reads your keyboard and it does not accept a password. There is no password capture: a line typed while a password or secret prompt is or may be active is not stored and is not sent to the model.
  • Remembered commands are redacted before a whisper sees them. Assignments matching api_key, token, secret, or password / passwd, PGPASSWORD and MYSQL_PWD, everything after a separated -p or --password, a password glued on -p, and hex or base64 blobs of 24 characters or more are replaced. URL userinfo, bearer tokens, and common key shapes are redacted as well.
  • --no-ai does not call Ollama. Nothing is sent to the model. Whispers use the local fallback, which still names you and the last redacted command when one exists. explain then prints the spirits are silent.
  • Haunting pauses on password prompts (and when pty echo is off) and while a full-screen app owns the alternate screen (?1049, ?1047, or ?47).
  • The terminal is always restored on finish, Ctrl+C, SIGTERM, and SIGHUP: cursor visible, attributes reset, main screen, raw mode off. Scares put back only the modes they turned on (cursor, attributes, alternate screen, inverse video). Focus reporting stays with the wrapper, which turns it off with \x1b[?1004l. Ctrl+C during a possession exits 130. SIGTERM exits 143. SIGHUP during wrap exits 129. In wrap, SIGINT is forwarded to the child (a pty is sent Ctrl+C; inherited stdio already received the signal).
  • Gemma replies and Entire ids, prompts, and file names are stripped of terminal escape sequences (OSC 52, OSC 8, CSI, and the other controls stripTerminalControls removes) before they are shown.
  • Prompts go to the configured Ollama URL and nowhere else. The default is loopback, http://127.0.0.1:11434. --ollama-url and --model change that target. The default model is gemma3:1b.

Flags

CLI flags win over the environment variables below, which win over the defaults.

FlagEffect
--calmWrap and shell never take over (no random haunts and no idle haunts), and the shell will not remark on a command you submitted. demo still plays its one scare.
--frequency <level>low, medium (default), high, or a number from 0 to 1.
--idle <seconds>Focused and untouched for this long, then again at a random 0.8×–1.4× gap. Default 10. 0 disables idle haunting only.
--no-idleNo idle haunting. Random haunts still follow --frequency.
--demoPlay the scripted haunting immediately.
--no-aiDo not call the local model.
--model <name>Ollama model. Default gemma3:1b.
--ollama-url <url>Ollama base URL. Default http://127.0.0.1:11434.
--os <name>demo, wrap, and shell: linux, mac, or windows. grimoire: linux, mac, windows, cross, or current.
--scare <type>ghostTyping, kernelPanic, deletionStream, screenTear, slowRed, or fakeSudo. demo also accepts all.
--severity <level>grimoire only: catastrophic, critical, high, medium.
--category <name>grimoire only.
--search <query>grimoire only.
--limit <n>grimoire only. Positive integer.
--randomgrimoire only. One lesson card.
--jsonexplain and grimoire only.
--entireAllow Entire recall. Default when entire is on PATH.
--no-entireDo not run Entire queries.
--helpShow usage.
VariableValues
HAUNTED_CALM1, true, yes, on, or 0, false, no, off. On also disables idle haunting.
HAUNTED_NO_AIsame boolean pairs
HAUNTED_FREQUENCYlow, medium, high, or a number from 0 to 1
HAUNTED_IDLESeconds, same as --idle. 0 disables idle haunting only.
HAUNTED_FAST1 or true skips theatrical delays. A piped stdout skips them too.
HAUNTED_NO_PTY1 or true skips node-pty
NO_COLORany value disables color and the cursor-hiding sequences
HAUNTED_TERMset to 1 in the wrapped child's environment

In a pty shell, a submitted line sometimes gets a one-line remark (about 12% of the time). That remark is display text. It is not written into the pty. It is not given a whisper label. A possession whisper is not labeled on the line itself. The lesson card footer names it once, for example whisper: gemma3:1b (local), or whisper: fallback when the local lines are used. doctor and --help show the same model credit. Checkpoint lore still prints [whispered by gemma3:1b via Ollama] or [fallback spirit].

The Grimoire

data/grimoire.json holds 141 entries. Counts were read from that file.

OSEntries
linux24
posix22
mac16
windows30
cross49
CategoryEntries
filesystem23
disk19
system17
security13
database12
git10
cloud10
process7
permissions6
network5
package5
container5
kubernetes5
history4

Severity in the same file: catastrophic 57, critical 49, high 31, medium 4.

To add a lesson, append one object to the JSON array. loadCatalog validates it on the next read. Full notes are in CONTRIBUTING.md. The shape:

{
  "id": "rm-rf-slash",
  "command": "rm -rf /",
  "os": "posix",
  "shell": "bash",
  "severity": "catastrophic",
  "category": "filesystem",
  "what": "Recursively deletes from the filesystem root slash.",
  "danger": "GNU rm may refuse / unless --no-preserve-root is set, but other rm tools wipe the machine anyway.",
  "safer": "Name one directory you intend to remove. Never point rm -rf at /.",
  "tags": ["rm -rf", "root", "slash", "wipe"]
}
  • id: unique kebab-case.
  • os: linux, mac, windows, posix, or cross.
  • severity: catastrophic, critical, high, or medium.
  • category: filesystem, disk, permissions, process, network, git, database, container, kubernetes, cloud, package, security, system, or history.
  • shell: a non-empty string such as bash, zsh, powershell, cmd, sql, or any.
  • what, danger, safer: non-empty strings. tags: a non-empty array of non-empty strings. An exact tag is an alias for explain.

Do not run the command to see what it does.

Architecture

ModuleRole
bin/haunted-term.jsShebang entry. Runs the CLI and exits after stdout and stderr flush.
src/cli.jsParses args into a config object and dispatches.
src/config.jsDefaults, env overrides, frequency parsing, spirit labels.
src/banner.jsGhost and HAUNTED-TERM wordmark.
src/possession.jsScripted takeover and the scheduler. Display only.
src/effects.jsTypewriter, flicker, glitch, noise, and signal restore.
src/terminal.jsLeave the alternate screen, show the cursor, drop raw mode, reap a wrapped child on the way out.
src/text.jsDisplay width, wrapping, stripTerminalControls, and sanitizeDisplayedText.
src/catalog.jsLoad, validate, filter, and search data/grimoire.json.
src/learn.jsexplain and grimoire rendering.
src/wrapper.jsPty or spawn of the user command, password guard, recent-command ring.
src/recent.jsLast five submitted command lines. Password prompts are left out.
src/redact.jsRedacts assignment secrets, separated and glued -p / --password, PGPASSWORD / MYSQL_PWD, URL userinfo, and long hex or base64 blobs.
src/ai.jsLocal Gemma over Ollama's HTTP API. Whisper prompt, output limits.
src/fallback.jsLines used when Gemma cannot speak, including the named fallback.
src/entire.jsAllowlisted read-only Entire queries and the haunt-entire scare.
src/doctor.jsEnvironment report.
src/exit.jsWait for a filled pipe to drain, then exit.
data/grimoire.jsonThe catalog.

Testing

npm test

That runs node --test test/*.test.js: cli, catalog, explain, possession, effects through the possession suite, ai, recent, wrapper, entire, and safety. Tests that check "never execute" spy on spawn and execFile. They do not run catalog strings.

Hacktoberfest

haunted-term is built for the DEV Hacktoberfest challenge Ghost in the Machine: a terminal that becomes haunted while an agent works, with the lesson written down so the scare teaches something. The open-source model in the loop is local Gemma.

Credits

  • Charan Nihaal R
  • Gemma by Google DeepMind
  • Ollama
  • Entire
  • Cursor CLI
  • DEV Hacktoberfest challenge "Ghost in the Machine"

License

MIT. See LICENSE. Copyright (c) 2026 Charan Nihaal R.