haunted-term
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:1bathttp://127.0.0.1:11434.ollama pull gemma3:1b ollama servehaunted-term doctorreports Node, whether Ollama answersGET /api/tags, whethergemma3:1bis listed,node-pty, and the Entire CLI. A model name without a tag is compared as:latest, sogemma3does not count asgemma3:1b. -
Optional:
node-pty. If it is missing, orHAUNTED_NO_PTY=1is 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.
| Setting | Gap between scares |
|---|---|
low or 0.25 | 8–15 minutes |
medium or 0.5 (default) | 3–6 minutes |
high or 1 | 45–90 seconds |
0 | never on the random timer (idle haunting can still run) |
| any other number from 0 to 1 | scaled 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.
| Type | What you see |
|---|---|
ghostTyping | A shell prompt, then a catalog command typed on screen, then a progress bar. |
kernelPanic | A kernel panic on Linux and macOS, or a blue screen (:(, stop code CRITICAL_PROCESS_DIED) on Windows. |
deletionStream | Directory names streaming past as removal lines. The names are read with readdir. |
screenTear | Shifted blocks, inverse video, and a bell. |
slowRed | A whisper, then a countdown from 10 to 1, then a cleared screen. |
fakeSudo | The 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.
- Exact. The query matches a catalog
commandor atagsentry after case and whitespace are folded. That row wins. - Fuzzy. Otherwise the search scores
command,id,tags,what, andcategory. A score above0.75is a catalog lesson, with up to three related spirits. - 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 isthe 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 --jsonthenentire checkpoint listentire session list --jsonthenentire 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, orpassword/passwd,PGPASSWORDandMYSQL_PWD, everything after a separated-por--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-aidoes 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.explainthen printsthe 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
stripTerminalControlsremoves) 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-urland--modelchange that target. The default model isgemma3:1b.
Flags
CLI flags win over the environment variables below, which win over the defaults.
| Flag | Effect |
|---|---|
--calm | Wrap 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-idle | No idle haunting. Random haunts still follow --frequency. |
--demo | Play the scripted haunting immediately. |
--no-ai | Do 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. |
--random | grimoire only. One lesson card. |
--json | explain and grimoire only. |
--entire | Allow Entire recall. Default when entire is on PATH. |
--no-entire | Do not run Entire queries. |
--help | Show usage. |
| Variable | Values |
|---|---|
HAUNTED_CALM | 1, true, yes, on, or 0, false, no, off. On also disables idle haunting. |
HAUNTED_NO_AI | same boolean pairs |
HAUNTED_FREQUENCY | low, medium, high, or a number from 0 to 1 |
HAUNTED_IDLE | Seconds, same as --idle. 0 disables idle haunting only. |
HAUNTED_FAST | 1 or true skips theatrical delays. A piped stdout skips them too. |
HAUNTED_NO_PTY | 1 or true skips node-pty |
NO_COLOR | any value disables color and the cursor-hiding sequences |
HAUNTED_TERM | set 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.
| OS | Entries |
|---|---|
| linux | 24 |
| posix | 22 |
| mac | 16 |
| windows | 30 |
| cross | 49 |
| Category | Entries |
|---|---|
| filesystem | 23 |
| disk | 19 |
| system | 17 |
| security | 13 |
| database | 12 |
| git | 10 |
| cloud | 10 |
| process | 7 |
| permissions | 6 |
| network | 5 |
| package | 5 |
| container | 5 |
| kubernetes | 5 |
| history | 4 |
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, orcross.severity:catastrophic,critical,high, ormedium.category:filesystem,disk,permissions,process,network,git,database,container,kubernetes,cloud,package,security,system, orhistory.shell: a non-empty string such asbash,zsh,powershell,cmd,sql, orany.what,danger,safer: non-empty strings.tags: a non-empty array of non-empty strings. An exact tag is an alias forexplain.
Do not run the command to see what it does.
Architecture
| Module | Role |
|---|---|
bin/haunted-term.js | Shebang entry. Runs the CLI and exits after stdout and stderr flush. |
src/cli.js | Parses args into a config object and dispatches. |
src/config.js | Defaults, env overrides, frequency parsing, spirit labels. |
src/banner.js | Ghost and HAUNTED-TERM wordmark. |
src/possession.js | Scripted takeover and the scheduler. Display only. |
src/effects.js | Typewriter, flicker, glitch, noise, and signal restore. |
src/terminal.js | Leave the alternate screen, show the cursor, drop raw mode, reap a wrapped child on the way out. |
src/text.js | Display width, wrapping, stripTerminalControls, and sanitizeDisplayedText. |
src/catalog.js | Load, validate, filter, and search data/grimoire.json. |
src/learn.js | explain and grimoire rendering. |
src/wrapper.js | Pty or spawn of the user command, password guard, recent-command ring. |
src/recent.js | Last five submitted command lines. Password prompts are left out. |
src/redact.js | Redacts assignment secrets, separated and glued -p / --password, PGPASSWORD / MYSQL_PWD, URL userinfo, and long hex or base64 blobs. |
src/ai.js | Local Gemma over Ollama's HTTP API. Whisper prompt, output limits. |
src/fallback.js | Lines used when Gemma cannot speak, including the named fallback. |
src/entire.js | Allowlisted read-only Entire queries and the haunt-entire scare. |
src/doctor.js | Environment report. |
src/exit.js | Wait for a filled pipe to drain, then exit. |
data/grimoire.json | The 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.