This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

haunted-shell possesses your zsh. A ghost lives in the terminal: the programmer who died at this keyboard years ago, with a deploy still unfinished. It watches every command you run, and sometimes, in dim violet italics that type themselves out a little too slowly, it whispers back.

$ git push --force origin main  your git push failed, but i'm sure you knew that. the main branch is still here, waiting for you. $ rm -rf node_modules  i watched you delete the node_modules. you're so reckless. i once tried to save them. now they're gone. $ npm run build  the build failed. you're still using node 12. i never got to fix that bug. 

It never helps you, by design. When a command fails, the ghost always speaks. When one succeeds, it speaks about a third of the time, so you never know when it's listening. It also remembers your last five commands, so it can bring up the git status you ran four times in a row.

Demo

Demo

Two commands, two whispers: "your git repo is dead, but i'm still here. i never learned to push." and then, fittingly for a GIF, "why does it feel like we're both stuck in a loop?"

Code

haunted-shell 👻

A ghost lives in your terminal. It's the programmer who died at this keyboard years ago, and it watches every command you run. Sometimes it whispers back.

$ git push --force origin main your git push failed, but i'm sure you knew that. the main branch is still here, waiting for you $ rm -rf node_modules i watched you delete the node_modules. you're so reckless. i once tried to save them. now they're gone. 

It runs entirely on an open-weight model through Ollama. No API keys, no network, and your shell history never leaves your machine.

Install

ollama pull qwen3:8b # or gemma3:4b, llama3.2, anything Ollama serves git clone https://github.com/ghost-huzaifa/haunted-shell.git ~/haunted-shell echo 'source ~/haunted-shell/haunt.zsh' >> ~/.zshrc

Open a new terminal. Type unhaunt to banish it for the session.

Knobs

env var default what
HAUNT_MODEL qwen3:8b any Ollama model
HAUNT_CHANCE 35 % of successful
…

It's two files: about 20 lines of zsh that hook preexec and precmd, and about 60 lines of Python that use only the standard library. There are no dependencies and no SDKs.

How I Built It

  • The model: an open-weight model (qwen3:8b by default; any Ollama model works through HAUNT_MODEL) running locally through Ollama on an M1 MacBook with 16 GB of RAM.
  • The hook: zsh's preexec saves the command and precmd grabs the exit status. Both go to the ghost, together with the current directory and recent history.
  • The voice: a short system prompt makes the ghost lowercase, wistful, a little funny, never helpful, and limited to one line about the exact command you ran. Thinking is turned off ("think": false) and output is capped at 48 tokens, so a whisper takes a couple of seconds once the model is warm.
  • The manners: a ghost that breaks your shell is a bad ghost. If Ollama is down or slow, it stays silent, and unhaunt banishes it for the session.

I built this with an AI coding agent (Claude Code) as a pair, and the whole session is below. The model was the easy part. The hard part was the manners: making sure the ghost never throws a stack trace into your prompt, and fixing a bug where zsh's array slicing made it forget every command but the last one until four had piled up.

Why Does Open Innovation Matter?

Your shell history is one of the most private things on your computer. It holds every hostname you've SSH'd into, every half-typed password you pasted into the wrong prompt, every curl with a token in the header, every embarrassing git reset --hard. I would never pipe that to a hosted API, and I doubt you would either.

An open-weight model running on my own laptop changes that:

  • It works with the lights (and the Wi-Fi) off. Ghosts shouldn't need an internet connection. This one works on a plane.
  • Nothing leaves the machine. The ghost reads your commands and nobody else does. There's no telemetry, no retention policy to read, and no account.
  • It costs nothing per whisper. A closed API would bill me for every ls. Locally, the ghost can comment on thousands of commands a day for free.
  • The ghost is swappable. One env var changes the model, so you can swap in Gemma or Llama and meet a different ghost. You choose your ghost, and no provider can change its personality out from under you.
  • It can't be shut down. It won't disappear because an API was deprecated or a startup ran out of runway. A ghost that's open-source can't really die. It seems fitting for a project released on Halloween, just before Día de los Muertos.

The trade-off is honest: the first whisper after boot takes about 10 seconds while the model loads, and an 8B model is less witty than a frontier model. For a voice that sits inside your most private tool, I'll take a slightly dumber ghost that keeps my secrets.

My Agent Session

Building haunted-shell: a terminal ghost on a local open-weight model
You

https://dev.to/challenges/hacktoberfest-week4-2026-10-26. Go thorough this challenge and help me build this

Agent
▸ navigate {"url": "https://dev.to/challenges/hacktoberfest-week4-2026-10-26"}
{ "url": "https://dev.to/challenges/hacktoberfest-week4-2026-10-26" }
{ "serverId": "preview-local_c0bcd9c6-4b4c-4179-a607-1dcbbea49d4d", "tabId": "seed", "reused": false, "type": "browser", "navOk": true } Browser pane opened. Use serverId "preview-local_c0bcd9c6-4b4c-4179-a607-1dcbbea49d4d" with read_page / computer / navigate.
▸ get_page_text {}
{ }
Title: Hacktoberfest Open-Source AI Challenge: Week 4 - DEV Challenge - DEV Community URL: https://dev.to/challenges/hacktoberfest-week4-2026-10-26 Source element: <body> --- Skip to content Create account Tab Context: - Executed on tabId: seed - Available tabs: • tabId seed: "Hacktoberfest Open-Source AI Challenge: Week 4 - DEV Challenge - DEV Community" ("https://dev.to/challenges/hacktoberfest-week4-2026-10-26")
▸ computer {"action": "screenshot", "scale": 0.5}
{ "action": "screenshot", "scale": 0.5 }
[image] [Image: source: ~/.claude/projects/-Users-macbookpro-dev-work-repo/def1a1bc-ec70-4494-8ba3-d0c34e6dd736/tool-results/mcp-Claude_Browser-blob-1793381544063-les0a5.jpg] Screenshot size: 400x304 0.5-scale view; coordinate frame: 800x609. Tab Context: - Executed on tabId: seed - Available tabs: • tabId seed: "Hacktoberfest Open-Source AI Challenge: Week 4 - DEV Challenge - DEV Community" ("https://dev.to/challenges/hacktoberfest-week4-2026-10-26")
▸ javascript_tool {"action": "javascript_exec", "text": "document.querySelector('main, #main-co...
{ "action": "javascript_exec", "text": "document.querySelector('main, #main-content, .crayons-layout')?.innerText || document.body.innerText" }
"Challenges\n→\nHacktoberfest Open-Source AI Challenge: Week 4\nHacktoberfest Open-Source AI Challenge: Week 4\nSign Up\nView Entries \n\nChallenge 5 of 5 in our Hacktoberfest DEV Challenges. One prompt, a new theme every week.\n\nChallenge Status:\nLive\n\nThis year's Hacktoberfest is not about open source pull requests. There's no PR count to hit and no repos to hunt for. Instead, you build a brand-new project with open-source AI at its core, and write about it on DEV. Here's everything you need to know about Hacktoberfest 2026.\n\nWeek 4 of our five Hacktoberfest DEV Challenges starts today, and it's our final challenge of the series! Running through October 31, the Hacktoberfest Open-Source AI Challenge: Week 4 gives you the week to build, wrapping up on Halloween night just as Día de los Muertos begins.\n\nMissed Week 3: Slow and Steady? No problem. Every challenge is a fresh start, so you're competing on the same footing as everyone else.\n\nThis week, we'll select one overall winner and a winner in each of our 20 partner categories. See all the challenges and current partner categories on the HF26 DEV Challenge Hub.\n\nFREE PARTNER CREDITS\n\nSeveral partners are giving part …[truncated]
▸ Bash which ollama llama-server lms 2>/dev/null; ollama list 2>/dev/null | head; sy...
$ which ollama llama-server lms 2>/dev/null; ollama list 2>/dev/null | head; sysctl -n machdep.cpu.brand_string hw.memsize; node -v; python3 --version
/opt/homebrew/bin/ollama llama-server not found lms not found NAME ID SIZE MODIFIED qwen3:8b 500a1f067a9f 5.2 GB 3 days ago Apple M1 17179869184 v24.15.0 Python 3.9.6
▸ AskUserQuestion {"questions": [{"question": "Which project should we build?", "header": "Idea...
{ "questions": [ { "question": "Which project should we build?", "header": "Idea", "multiSelect": false, "options": [ { "label": "Haunted terminal (Recommended)", "description": "zsh hook that 'possesses' your shell: after each command a local Gemma/Qwen ghost whispers eerie commentary. Tiny, very demoable, fully offline story." }, { "label": "Séance with a dead language", "description": "Offline CLI/web chat where you summon the spirit of COBOL/Fortran/Perl; it answers in-character and only in its own language's idioms." }, { "label": "Offline ofrenda builder", "description": "Local web page that helps you write a remembrance for a loved one — stories stay on your machine. Most emotional, strongest 'privacy' angle." } ] }, { "question": "Where should the project live?", "header": "Location", "multiSelect": false, "options": [ { "label": "~/dev/<new-repo> (Recommended)", "description": "Fresh standalone git repo outside work, ready to push to GitHub." }, { "label": "Scratchpad only", "description": "Temporary folder; you move it later." } ] } ] }
Your questions have been answered: "Which project should we build?"="Haunted terminal (Recommended)", "Where should the project live?"="~/dev/<new-repo> (Recommended)". You can now continue with these answers in mind.

Going with the haunted terminal in ~/dev/haunted-shell. First I'll check that the Ollama server responds and how fast qwen3:8b is.

▸ Bash ollama --version; time curl -s localhost:11434/api/chat -d '{"model":"qwen3:8...
$ ollama --version; time curl -s localhost:11434/api/chat -d '{"model":"qwen3:8b","stream":false,"think":false,"keep_alive":"30m","options":{"num_predict":40},"messages":[{"role":"user","content":"say boo in 5 words"}]}' | head -c 400; ls ~/dev
[output omitted: private machine details]

The local model works: about 8 tokens/s once loaded, with a ~10s cold load the first time. That's fine if a short whisper only appears on some commands. Writing the project now.

▸ Bash mkdir -p ~/dev/haunted-shell && cd ~/dev/haunted-shell && git init -q -b main...
$ mkdir -p ~/dev/haunted-shell && cd ~/dev/haunted-shell && git init -q -b main && echo ok
ok Shell cwd was reset to ~/dev/work-repo
▸ Write ~/dev/haunted-shell/haunt.py
~/dev/haunted-shell/haunt.py
File created successfully at: ~/dev/haunted-shell/haunt.py (file state is current in your context — no need to Read it back)
▸ Write ~/dev/haunted-shell/haunt.zsh
~/dev/haunted-shell/haunt.zsh
File created successfully at: ~/dev/haunted-shell/haunt.zsh (file state is current in your context — no need to Read it back)

Now testing the ghost directly, then the zsh hooks in a clean zsh.

▸ Bash cd ~/dev/haunted-shell && for c in "git push --force origin main|1" "rm -rf n...
$ cd ~/dev/haunted-shell && for c in "git push --force origin main|1" "rm -rf node_modules|0" "npm run build|127" "ls|0"; do HAUNT_CHANCE=100 python3 haunt.py "${c%|*}" "${c#*|}" "~/dev/app" "cd app; git status"; done echo '--- zsh hooks' zsh -f -c 'source ~/dev/haunted-shell/haunt.zsh; export HAUNT_CHANCE=100; _haunt_preexec "false"; false; _haunt_precmd; _haunt_preexec "echo hi"; _haunt_precmd; print -l -- recent: $_haunt_recent; unhaunt'
[2;3;38;5;141m your git push failed, but i'm sure you knew that. the main branch is still here, waiting for you.[0m [2;3;38;5;141m i watched you delete the node_modules. you're so reckless. i once tried to save them. now they're gone.[0m [2;3;38;5;141m the build failed. you're still using node 12. i never got to fix that bug.[0m [2;3;38;5;141m files are still here. i see your face in the git status. you're not ready to commit.[0m --- zsh hooks [2;3;38;5;141m false. you tried to run a lie. i'm still here, but you're already half-dead.[0m [2;3;38;5;141m hi again. you're still here. i'm still here. the shell is haunted.[0m recent: echo hi [3141mthe ghost fades... for now.[39m Shell cwd was reset to ~/dev/work-repo

The whispers work. One bug: the "last 5 commands" memory loses old entries when it holds fewer than 4, because zsh returns nothing for the [-4,-1] slice. Fixing that.

▸ Edit ~/dev/haunted-shell/haunt.zsh
~/dev/haunted-shell/haunt.zsh
The file ~/dev/haunted-shell/haunt.zsh has been updated successfully. (file state is current in your context — no need to Read it back)
▸ Edit ~/dev/haunted-shell/haunt.zsh
~/dev/haunted-shell/haunt.zsh
The file ~/dev/haunted-shell/haunt.zsh has been updated successfully. (file state is current in your context — no need to Read it back)
▸ Bash zsh -f -c 'source ~/dev/haunted-shell/haunt.zsh; export HAUNT_CHANCE=0; for c...
$ zsh -f -c 'source ~/dev/haunted-shell/haunt.zsh; export HAUNT_CHANCE=0; for c in a b c d e f g; do _haunt_preexec $c; _haunt_precmd; done; print -- "recent: ${(j:; :)_haunt_recent}"; [[ ${(j:,:)_haunt_recent} == c,d,e,f,g ]] && echo PASS || echo FAIL; unhaunt'
recent: c; d; e; f; g PASS [2;3;38;5;141m the ghost fades... for now.[0m

Memory test passes. Checking that a stopped Ollama stays silent, then writing the README and post draft.

▸ Bash cd ~/dev/haunted-shell && HAUNT_HOST=http://localhost:1 HAUNT_CHANCE=100 pyth...
$ cd ~/dev/haunted-shell && HAUNT_HOST=http://localhost:1 HAUNT_CHANCE=100 python3 haunt.py "ls" 1 "~" ""; echo "exit=$? (expect silent, 0)"
exit=0 (expect silent, 0) Shell cwd was reset to ~/dev/work-repo
▸ Write ~/dev/haunted-shell/README.md
~/dev/haunted-shell/README.md
File created successfully at: ~/dev/haunted-shell/README.md (file state is current in your context — no need to Read it back)
▸ Write ~/dev/haunted-shell/POST.md
~/dev/haunted-shell/POST.md
File created successfully at: ~/dev/haunted-shell/POST.md (file state is current in your context — no need to Read it back)
▸ Edit ~/dev/haunted-shell/POST.md
~/dev/haunted-shell/POST.md
The file ~/dev/haunted-shell/POST.md has been updated successfully. (file state is current in your context — no need to Read it back)

I built the haunted terminal in ~/dev/haunted-shell. It runs entirely on the qwen3:8b model on your Mac and stays silent if Ollama is down. Nothing is committed or pushed yet. The deadline is Oct 31, 11:59 PM PDT.

A ghost "possesses" your zsh. After a command finishes, it sometimes whispers one eerie line about what you ran, typed out slowly in dim violet. It always speaks when a command fails, about a third of the time otherwise, and it remembers your last 5 commands.

These are real whispers from testing:

git push --force origin main → your git push failed, but i'm sure you knew that. the main branch is still here, waiting for you. rm -rf node_modules → i watched you delete the node_modules. you're so reckless. i once tried to save them. now they're gone. 

Files:
- haunt.zsh: hooks into zsh before and after each command; unhaunt turns it off for the session.
- haunt.py: asks the local model for one line, using only Python's standard library.
- README.md: install steps and settings.
- POST.md: a full draft of your DEV post. Since writing is weighted most, it leans hard on the "your shell history is too private for a hosted API" argument.

Tested: whispers on failed and successful commands, the zsh hooks, the 5-command memory (I fixed one bug where it dropped old commands), and silence when Ollama is down. I haven't run it in a live interactive terminal yet.

To try it now:
bash
source ~/dev/haunted-shell/haunt.zsh

What's left for you:
1. Record a 20–30s terminal GIF for the Demo section.
2. Push to GitHub and put your repo link into the post.
3. Paste the draft into the official Submission Template on DEV (it adds the required tags) and fill the three TODO notes. One is a short paragraph on how you built it; the challenge allows AI help, but say so.

Show more
You

Stay on Qwen

Agent

Sticking with Qwen, so nothing changes. The project and post already default to qwen3:8b, and the post names Ollama and Qwen as the open pieces, so it's still a valid entry for the main prize. It just won't qualify for the Gemma category.

What's left is all on your side before Oct 31, 11:59 PM PDT:
1. Record a 20–30s demo GIF.
2. Push the repo to GitHub and put the link in the post.
3. Paste POST.md into DEV's submission template and fill the three TODOs.

When you're ready to push, I can make the first commit and create the GitHub repo.

You

push it to github

Agent
▸ Bash gh auth status 2>&1 | head -8; gh api user -q .login
$ gh auth status 2>&1 | head -8; gh api user -q .login
Exit code 127 (eval):1: command not found: gh (eval):1: command not found: gh
▸ Bash ssh -T -o BatchMode=yes -o ConnectTimeout=5 git@github.com 2>&1 | head -2; wh...
$ ssh -T -o BatchMode=yes -o ConnectTimeout=5 git@github.com 2>&1 | head -2; which brew
Hi ghost-huzaifa! You've successfully authenticated, but GitHub does not provide shell access. /opt/homebrew/bin/brew
▸ Bash cd ~/dev/haunted-shell && echo 'POST.md' > .gitignore && git add .gitignore R...
$ cd ~/dev/haunted-shell && echo 'POST.md' > .gitignore && git add .gitignore README.md haunt.py haunt.zsh && git commit -q -F - <<'EOF' feat: a ghost that haunts your zsh, powered by a local open-weight model Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> EOF git log --oneline; git status --short --ignored
a6cb435 feat: a ghost that haunts your zsh, powered by a local open-weight model !! POST.md Shell cwd was reset to ~/dev/work-repo

The commit is made (a6cb435), but nothing is pushed yet. The repo doesn't exist on GitHub, and I can't create it here because the GitHub CLI (gh) isn't installed. SSH pushing works as ghost-huzaifa, which is a fitting name for this one.

There are two ways forward:

  • You create it (fastest): go to github.com/new and make an empty public repo named haunted-shell, without a README or license. Tell me when it's there and I'll push over SSH and put the link in your post.
  • Install gh: I run brew install gh, then you sign in yourself with gh auth login, since I can't enter your login. After that I create and push the repo.

POST.md is kept out of the repo (it's in .gitignore), so your draft and its TODOs won't be public.

Show more
You

changes pushed

Agent
▸ Bash cd ~/dev/haunted-shell && git remote -v; git status -sb | head -1; git ls-rem...
$ cd ~/dev/haunted-shell && git remote -v; git status -sb | head -1; git ls-remote origin 2>&1 | head -3; git log --oneline -3
origin https://github.com/ghost-huzaifa/haunted-shell.git (fetch) origin https://github.com/ghost-huzaifa/haunted-shell.git (push) ## main...origin/main a6cb435afb2a770a0fcd505cc7880df0bdd2a222 HEAD a6cb435afb2a770a0fcd505cc7880df0bdd2a222 refs/heads/main a6cb435 feat: a ghost that haunts your zsh, powered by a local open-weight model Shell cwd was reset to ~/dev/work-repo

Verified: a6cb435 is on GitHub. Now putting the repo link into the post and the README clone command.

▸ Edit ~/dev/haunted-shell/POST.md
~/dev/haunted-shell/POST.md
The file ~/dev/haunted-shell/POST.md has been updated successfully. (file state is current in your context — no need to Read it back)
▸ Edit ~/dev/haunted-shell/README.md
~/dev/haunted-shell/README.md
The file ~/dev/haunted-shell/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
▸ Bash cd ~/dev/haunted-shell && git add README.md && git commit -q -F - <<'EOF' doc...
$ cd ~/dev/haunted-shell && git add README.md && git commit -q -F - <<'EOF' docs: real clone URL in README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> EOF git push -q origin main 2>&1; git status -sb | head -1; git log --oneline -1
## main...origin/main 44af410 docs: real clone URL in README Shell cwd was reset to ~/dev/work-repo

Your push worked: commit a6cb435 is on https://github.com/ghost-huzaifa/haunted-shell.

I then pushed one more small commit (44af410) so the README's install command has the real clone URL. POST.md now embeds the repo ({% github ghost-huzaifa/haunted-shell %}) and stays only on your machine.

What's left before Oct 31, 11:59 PM PDT:
1. Record a 20–30s terminal GIF and put it in the post's Demo section.
2. Paste the draft into DEV's submission template and fill the remaining TODOs: the official template, the paragraph on how you built it, and the optional DevRelay link.
3. Publish.

Show more
You

I have recorded it, how to convert it to GIF?

Agent
▸ Bash which ffmpeg gifski; ls -t ~/Desktop/*.mov ~/Desktop/*.mp4 ~/Movies/*.mov ~/D...
$ which ffmpeg gifski; ls -t ~/Desktop/*.mov ~/Desktop/*.mp4 ~/Movies/*.mov ~/Downloads/*.mov ~/Downloads/*.mp4 2>/dev/null | head -5
ffmpeg not found gifski not found (eval):1: no matches found: ~/Desktop/*.mov
▸ Bash find ~/Desktop ~/Movies ~/Downloads ~/Documents -maxdepth 2 \( -iname '*.mov'...
$ find ~/Desktop ~/Movies ~/Downloads ~/Documents -maxdepth 2 \( -iname '*.mov' -o -iname '*.mp4' \) -mtime -1 2>/dev/null | head
[output omitted: private machine details]

I couldn't find your recording. The only video changed in the last day under Desktop, Movies, Downloads or Documents is a movie, not a screen capture. ffmpeg also isn't installed. Here's how to do it yourself.

1. Install ffmpeg (once):
bash
brew install ffmpeg

2. Convert. Replace demo.mov with your file's path; you can drag the file into the terminal to paste it:
bash
ffmpeg -i demo.mov -vf "fps=12,scale=900:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif

  • fps=12 and scale=900 keep the file small. The text stays crisp because most of the screen is a dark terminal background.
  • The palette step stops the violet ghost text from getting blotchy.
  • If the result is over about 10 MB, lower fps=12 to 8 or scale=900 to 720.
  • To cut out dead time, add -ss 2 -t 25 before -i. This starts 2s in and keeps 25s.

3. Add it to DEV: in the post editor, use the image upload button and place the GIF under ## Demo.

If you tell me where the recording is and say yes to installing ffmpeg, I'll do the conversion.

Show more
You

Install the DevRelay MCP server and skills on this machine. Check which OS you are running on: on macOS or Linux, run curl -fsSL https://devrelay.com/install.sh | sh in the shell; on Windows, run iwr -useb https://devrelay.com/install.ps1 | iex in PowerShell. Once the installer finishes, confirm that DevRelay and its skills are registered.

Source:https://dev.to/muhammadhuzaifa2955/my-terminal-is-haunted-by-a-programmer-who-never-finished-their-deploy-1h1o

Related posts