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
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:8bby default; any Ollama model works throughHAUNT_MODEL) running locally through Ollama on an M1 MacBook with 16 GB of RAM. -
The hook: zsh's
preexecsaves the command andprecmdgrabs 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
unhauntbanishes 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
https://dev.to/challenges/hacktoberfest-week4-2026-10-26. Go thorough this challenge and help me build this
{
"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.
{
}
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")
{
"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")
{
"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]
$ 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
{
"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.
$ 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.
$ 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
~/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)
~/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.
$ 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.
~/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)
~/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)
$ 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.
$ 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
~/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)
~/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)
~/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.
Stay on Qwen
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.
push it to github
$ 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
$ 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
$ 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 runbrew install gh, then you sign in yourself withgh 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.
changes pushed
$ 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.
~/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)
~/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)
$ 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.
I have recorded it, how to convert it to GIF?
$ 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
$ 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=12andscale=900keep 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=12to8orscale=900to720. - To cut out dead time, add
-ss 2 -t 25before-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.
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.
On macOS, so the shell installer applies. I'll download the script and read it before running it.
$ D=<scratch>/devrelay && mkdir -p $D && curl -fsSL https://devrelay.com/install.sh -o $D/install.sh; echo "exit=$?"; wc -l $D/install.sh; cat $D/install.sh
<persisted-output>
Output too large (42.8KB). Full output saved to: ~/.claude/projects/-Users-macbookpro-dev-work-repo/def1a1bc-ec70-4494-8ba3-d0c34e6dd736/tool-results/baex0l6is.txt
Preview (first 2KB):
exit=0
997 <scratch>/devrelay/install.sh
#!/bin/sh
# DevRelay installer.
#
# curl -fsSL https://devrelay.com/install.sh | sh
#
# Installs the gateway binary, wires it into every AI coding host it can find,
# installs the agent skills, and schedules a daily update check.
#
# Flags (pass after `-s --` when piping, e.g. `curl ... | sh -s -- --no-telemetry`):
# --no-auto-update Skip the scheduled background update check
# --no-skills Skip installing agent skills
# --no-telemetry Opt out of usage telemetry
# --detailed-telemetry Also send anonymous per-tool-call events (opt-in)
# --no-path-update Skip adding DevRelay to shell profiles
# --no-login Skip the interactive browser sign-in prompt at the end
# --hosts=LIST Configure only these comma-separated AI host IDs
# --ref=LABEL Attribution label sent with telemetry (letters, digits, . _ -)
# --quiet Less output
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\ …[truncated]
$ F=<scratch>/devrelay/install.sh; grep -nE 'curl|wget|sha256|shasum|chmod|launchctl|crontab|LaunchAgents|sudo|rm -|eval|\| *sh|>> *"|mcp|skills|telemetry_|exec |login|open ' $F | grep -vE '^\s*[0-9]+:\s*#' | head -120
50: --no-skills) WANT_SKILLS=0 ;;
54: --no-login) WANT_LOGIN=0 ;;
69: curl -fsSL https://devrelay.com/install.sh | sh
71:Flags (after `-s --` when piping, e.g. `curl ... | sh -s -- --no-skills`):
73: --no-skills Skip installing agent skills
78: --no-login Skip the interactive browser sign-in prompt at the end
176:BINARY_PATH="${INSTALL_DIR}/dev_mlh_mcp_server"
185: if command -v curl >/dev/null 2>&1; then
186: curl -fsSL "$1" -o "$2" 2>/dev/null
187: elif command -v wget >/dev/null 2>&1; then
188: wget -q -O "$2" "$1" 2>/dev/null
204: chmod +x "${BINARY_PATH}.new" || return 1
218: rm -f "${BINARY_PATH}.tmp"
221: rm -f "${BINARY_PATH}.tmp"
224: chmod +x "${BINARY_PATH}.tmp"
237: for candidate in "./target/release/dev_mlh_mcp_server" "./target/debug/dev_mlh_mcp_server"; do
261: rm -rf "${BUILD_DIR}"
268: if (cd "${BUILD_DIR}" && cargo build --release --bin dev_mlh_mcp_server >/dev/null 2>&1); then
269: install_binary "${BUILD_DIR}/target/release/dev_mlh_mcp_server" \
277: [ "${BUILD_DIR}" = "." ] || rm -rf "${BUILD_DIR}"
293: cargo build --release --bin dev …[truncated]
The installer comes from MLH's DevRelay project, which matches the challenge page. Before running it, I'm checking how it downloads the binary and how it registers with Claude Code.
$ F=<scratch>/devrelay/install.sh; sed -n '160,232p;640,700p' $F; grep -nE 'claude|TELEMETRY|telemetry' $F | grep -v '^\s*#' | head -30
if [ "${PLATFORM_KEY}" = "linux-aarch64" ]; then
die "Unsupported platform: ${PLATFORM_KEY}. Published Linux binaries currently require x86_64."
fi
say "${GREEN}✓${RESET} Platform: ${PLATFORM_KEY}"
# Pick a JSON runtime once. Every AI host stores its MCP config as JSON, and
# editing that with sed is how you corrupt someone's editor settings.
JSON_RT=""
if command -v node >/dev/null 2>&1; then
JSON_RT="node"
elif command -v python3 >/dev/null 2>&1; then
JSON_RT="python3"
fi
INSTALL_DIR="${HOME}/.devrelay/bin"
CONFIG_DIR="${HOME}/.devrelay"
BINARY_PATH="${INSTALL_DIR}/dev_mlh_mcp_server"
mkdir -p "${INSTALL_DIR}"
# ------------------------------------------------------------------ binary ---
say "${CYAN}→${RESET} Installing binary to ${BINARY_PATH}"
fetch() {
# fetch <url> <dest> -> 0 on success
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1" -o "$2" 2>/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$2" "$1" 2>/dev/null
else
return 1
fi
}
# install_binary <source> — put a binary at BINARY_PATH via atomic rename.
#
# Never `cp` onto the destination directly. macOS caches code-signature
# validatio …[truncated]
$ sh <scratch>/devrelay/install.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | tail -60
telemetry basic
skills
✓ 11 installed in ~/.claude/skills
✓ 11 installed in ~/.agents/skills
✓ 10 installed in ~/.gemini/config/skills
✓ 9 installed in ~/.copilot/skills
✓ 13 installed in ~/.codex/skills
✓ 11 installed in ~/.cursor/skills
Claude Desktop
✓ MCP gateway registered
- skills come from your Claude account, not from disk
run `devrelay --export-skills`, then upload via
Settings → Capabilities → Skills
auto-update
✓ scheduled (launchd: ~/Library/LaunchAgents/com.devrelay.updater.plist)
✗ no update check has ever completed
run `devrelay --check-update`
clients
✓ Claude Code
✓ Cursor
✓ Antigravity
✓ Gemini CLI
✓ Codex
✓ GitHub Copilot CLI
✗ Zed (config exists, gateway not registered)
✗ VS Code (GitHub Copilot) (config exists, gateway not registered)
✓ Claude Desktop
3 issue(s) found.
What you can do with DevRelay in your coding agent:
• Community Wisdom: Search real-world DEV (dev.to) discussions and patterns while you code
• MLH Hackathons & Challenges: Find events, register, check in, and enter DEV Challenges
• Sponsor Perks & Agent Skills: Claim event promo code …[truncated]
Installation succeeded. Now checking for myself that Claude Code has the server and skills registered.
$ python3 -c "import json,os;d=json.load(open(os.path.expanduser('~/.claude.json')));print('claude-code mcp:',json.dumps(d.get('mcpServers',{}).get('devrelay-gateway')))"; ls -la ~/.devrelay/bin/; ~/.devrelay/bin/dev_mlh_mcp_server --version 2>&1 | head -2; ls -lt ~/.claude/skills | head -14
claude-code mcp: {"command": "~/.devrelay/bin/dev_mlh_mcp_server", "args": ["--stdio"]}
total 45536
drwxr-xr-x@ 5 macbookpro staff 160 Oct 30 22:47 .
drwxr-xr-x@ 8 macbookpro staff 256 Oct 30 22:47 ..
-rwxr-xr-x@ 1 macbookpro staff 23306320 Oct 30 22:47 dev_mlh_mcp_server
lrwxr-xr-x@ 1 macbookpro staff 50 Oct 30 22:47 devrelay -> ~/.devrelay/bin/dev_mlh_mcp_server
-rwxr-xr-x@ 1 macbookpro staff 373 Oct 30 22:47 devrelay-update
devrelay 0.1.27
total 0
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-sponsor-skills
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-sessions
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-publishing
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-offers
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-navigator
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-mlh-submissions
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-knowledge
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-community-wisdom
drwxr-xr-x@ 3 macbookpro staff 96 Oct 30 22:47 devrelay-challenges
drwxr-xr-x@ 3 macbookpro staff 96 Sep 28 14:19 tldraw-offline
drwxr-xr-x@ …[truncated]
DevRelay is installed on this Mac (macOS, so I used the shell installer), and it's registered with Claude Code. Before running it I downloaded the script and read it, then ran that same copy.
Confirmed registered:
- MCP server: ~/.claude.json has devrelay-gateway, pointing to ~/.devrelay/bin/dev_mlh_mcp_server --stdio. The binary reports version devrelay 0.1.27.
- Skills: 9 devrelay-* skills are in ~/.claude/skills, including devrelay-sessions, devrelay-publishing and devrelay-challenges. They already show up in this session.
- Other tools it set up: Claude Desktop, Cursor, Codex, Gemini CLI, Antigravity and GitHub Copilot CLI each got the MCP server, and most also got the skills.
What else it changed:
- A daily auto-update job at ~/Library/LaunchAgents/com.devrelay.updater.plist.
- ~/.devrelay/bin added to your shell PATH.
- Basic telemetry is on. Turn it off with devrelay telemetry off.
Not done:
- It couldn't register with Zed or VS Code's GitHub Copilot, even though their config files exist. I left those alone.
- Its health check says no update check has run yet, which is normal right after an install.
Two steps only you can do:
1. Sign in with your DEV or MLH account. This opens your browser:
bash
~/.devrelay/bin/devrelay login
2. Restart the Claude app so it picks up the new MCP server. Until then the DevRelay tools aren't available here.
After that, the devrelay-sessions skill can save this session so you can embed it in your post. That fills the optional DevRelay TODO in POST.md.
<bash-input>~/.devrelay/bin/devrelay login</bash-input><bash-stdout>Sign in with either the email you associate with MLH or your DEV (dev.to) account.
Waiting for authorization in your browser. If it did not open, visit:
https://www.mlh.com/oauth/authorize?response_type=code&client_id=devrelay-native&redirect_uri=http%3A%2F%2Flocalhost%3A8765%2Fcallback&scope=public+offline_access+user%3Aread%3Aprofile+user%3Aread%3Aeducation+user%3Aread%3Aemployment+dev%3Aread%3Aall+dev%3Awrite%3Aall+participant%3Aread%3Aevent+participant%3Aread%3Apromo_code+participant%3Awrite%3Apromo_code+participant%3Aread%3Aproject+participant%3Awrite%3Aproject+participant%3Awrite%3Aparticipation&state=<redacted>&code_challenge=<redacted>&code_challenge_method=S256
If the browser cannot reach the callback, paste the full callback URL here and press Enter:
Connected: Muhammad Huzaifa (MLH user <id>)
╔══════════════════════════════════════════════════════════════════════════╗
║ 🎉 ✓ You're signed in and DevRelay is ready! ║
╚══════════════════════════════════════════════════════════════════════════╝
┃ MLH account: Muhammad Huzaifa (MLH user <id>)
┃ DEV account: @muhammadhuzaifa2955 (connected on dev.to)
┃ MLH event: Hacktoberfest 2026 (registered)
✨ What you might do next in your coding agent:
• You're registered for Hacktoberfest 2026 (in progress)! Ask your agent:
▸ "Show me the prize categories, sponsor offers, and Agent Skills for Hacktoberfest 2026."
▸ "Help me prepare and submit my hackathon project to Hacktoberfest 2026."
• With your DEV (@muhammadhuzaifa2955) account connected, ask your agent:
▸ "What does the DEV community recommend for this library or bug?"
▸ "Stage a draft post on DEV about what we're building."
• Live DEV Challenge right now: "Hacktoberfest Open-Source AI Challenge: Week 4" — ask your agent:
▸ "Audit my project against the Hacktoberfest Open-Source AI Challenge: Week 4 rubric and help me draft an entry."
┌──────────────────────────────────────────────────────────────────────────┐
│ 🔄 Restart your AI host so it picks up the new MCP server. │
└──────────────────────────────────────────────────────────────────────────┘</bash-stdout><bash-stderr></bash-stderr>
Sign-in worked. Your MLH account, DEV account @muhammadhuzaifa2955 and Hacktoberfest 2026 registration are all connected.
I can't use DevRelay's tools in this session yet; I checked and none are loaded. Restart the Claude app so it loads the MCP server, then come back to this session.
After the restart I can:
1. Save this session to DEV and embed it in your post. That fills the DevRelay TODO.
2. Stage POST.md as an unpublished draft on your DEV account, so you only add the GIF and publish.
The deadline is Oct 31, 11:59 PM PDT.
<pasted_content id="f022">
macbookpro@MacBooks-MacBook-Pro haunted-shell % ffmpeg -i ~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23 PM.mov at 10.43.23 PM.mov -vf "fps=12,scale=900👎flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif
ffmpeg version 9.0.2 Copyright (c) 2000-2026 the FFmpeg developers
built with Apple clang version 21.0.0 (clang-2100.3.34.2)
configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/9.0.2_1 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gpl --enable-libsvtav1 --enable-libopus --enable-libx264 --enable-libmp3lame --enable-libdav1d --enable-libvmaf --enable-libvpx --enable-libx265 --enable-openssl --enable-videotoolbox --enable-audiotoolbox --enable-neon
libavutil 61. 1.102 / 61. 1.102
libavcodec 63. 1.102 / 63. 1.102
libavformat 63. 1.102 / 63. 1.102
libavdevice 63. 1.102 / 63. 1.102
libavfilter 12. 1.102 / 12. 1.102
libswscale 10. 1.102 / 10. 1.102
libswresample 7. 1.102 / 7. 1.102
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '~/Screenshots/Screen Recording 2026-10-30 at 10.43.23 PM.mov':
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
creation_time : 2026-10-30T17:43:25.000000Z
com.apple.quicktime.author: ReplayKitRecording
com.apple.quicktime.full-frame-rate-playback-intent: 1
Duration: 00:00:40.64, start: 0.000000, bitrate: 4105 kb/s
Stream #0:00x1: Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 2 kb/s, start 0.191521 (default)
Metadata:
creation_time : 2026-10-30T17:43:25.000000Z
handler_name : Core Media Audio
vendor_id : [0][0][0][0]
Stream #0:10x2: Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 3340x2104, 4409 kb/s, 8.60 fps, 60 tbr, 600 tbn (default)
Metadata:
creation_time : 2026-10-30T17:43:25.000000Z
handler_name : Core Media Video
vendor_id : [0][0][0][0]
encoder : H.264
[AVFormatContext @ 0x7b57023980] Unable to choose an output format for 'at'; use a standard extension for the filename or specify the format manually.
[out#0 @ 0x7b570243c0] Error initializing the muxer for at: Invalid argument
Error opening output file at.
Error opening output files: Invalid argument
</pasted_content id="f022">
The command broke because at 10.43.23 PM.mov appears twice; tab completion probably pasted it again. ffmpeg read that leftover at as the output filename. I'll run the conversion on your recording directly.
$ cd ~/dev/haunted-shell && ffmpeg -y -loglevel error -i ~/Screenshots/"Screen Recording 2026-10-30 at 10.43.23 PM.mov" -vf "fps=10,scale=900:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif && ls -lh demo.gif
Exit code 254
[in#0 @ 0x79af018000] Error opening input: No such file or directory
Error opening input file ~/Screenshots/Screen Recording 2026-10-30 at 10.43.23 PM.mov.
Error opening input files: No such file or directory
macOS puts a special narrow space before "PM" in screenshot filenames. Matching the file with a wildcard instead:
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -i "$f[1]" -vf "fps=10,scale=900:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif && ls -lh demo.gif
-rw-r--r--@ 1 macbookpro staff 2.1M Oct 30 22:52 demo.gif
Shell cwd was reset to ~/dev/work-repo
2.1 MB, which is fine for DEV. Checking a frame so I know the ghost text is readable:
$ cd <scratch> && ffmpeg -y -loglevel error -ss 30 -i ~/dev/haunted-shell/demo.gif -frames:v 1 frame.png && echo ok
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/frame.png
[image]
$ cd <scratch> && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -i "$f[1]" -vf "fps=1/4,scale=800:-1,tile=2x5" -frames:v 1 sheet.png && echo ok
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/sheet.png
[image]
$ cd <scratch> && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -ss 16 -i "$f[1]" -vf "crop=1500:330:220:225,eq=contrast=1.25:brightness=0.04" -frames:v 1 crop.png && echo ok
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/crop.png
[image]
Now it's readable, and that's a great whisper. Rendering the final GIF from seconds 3 to 31, where both whispers appear:
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -ss 3 -to 31 -i "$f[1]" -vf "crop=1500:330:220:225,eq=contrast=1.25:brightness=0.04,fps=10,scale=1000:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=64[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif && ls -lh demo.gif && ffmpeg -y -loglevel error -sseof -1 -i demo.gif -frames:v 1 <scratch>/last.png && echo ok
-rw-r--r--@ 1 macbookpro staff 728K Oct 30 22:52 demo.gif
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/last.png
[image]
The GIF ends on an already-cleared screen, so the loop would end on nothing. I'll find the exact second the screen clears so it ends on the last whisper instead:
$ cd <scratch> && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -ss 22 -t 9 -i "$f[1]" -vf "fps=1,crop=1500:330:220:225,scale=600:-1,tile=1x9" -frames:v 1 tail.png && echo ok
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/tail.png
[image]
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && ffmpeg -y -loglevel error -ss 3 -to 26.8 -i "$f[1]" -vf "crop=1500:330:220:225,eq=contrast=1.25:brightness=0.04,fps=10,scale=1000:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=64[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif && ls -lh demo.gif && ffmpeg -y -loglevel error -sseof -0.3 -i demo.gif -frames:v 1 <scratch>/last.png && echo ok
-rw-r--r--@ 1 macbookpro staff 605K Oct 30 22:53 demo.gif
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/last.png
[image]
It's cut off mid-sentence, because seeking in this variable-frame-rate recording isn't exact. I'll extend the end and check again:
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && S=<scratch> && ffmpeg -y -loglevel error -ss 26 -t 4 -i "$f[1]" -vf "fps=2,crop=1500:330:220:225,scale=500:-1,drawtext=text='%{pts\:hms}':x=380:y=5:fontcolor=yellow:fontsize=16,tile=1x8" -frames:v 1 $S/t2.png -copyts 2>&1 | head -3; echo done
[AVFilterGraph @ 0x7802c40480] No such filter: 'drawtext'
Error opening output file <scratch>/t2.png.
Error opening output files: Filter not found
done
Shell cwd was reset to ~/dev/work-repo
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && S=<scratch> && for e in 27.5 28.5; do ffmpeg -y -loglevel error -ss 3 -to $e -i "$f[1]" -vf "crop=1500:330:220:225" -update 1 $S/end-$e.png; done; echo ok
ok
Shell cwd was reset to ~/dev/work-repo
<scratch>/end-28.5.png
[image]
<scratch>/end-27.5.png
[image]
Ending at 0:28.5 catches the complete whisper. I'll also hold that last frame for 2.5 seconds so viewers can read it before the loop restarts:
$ cd ~/dev/haunted-shell && f=(~/Screenshots/Screen\ Recording\ 2026-10-30\ at\ 10.43.23*.mov) && S=<scratch> && ffmpeg -y -loglevel error -ss 3 -to 28.5 -i "$f[1]" -vf "crop=1500:330:220:225,eq=contrast=1.25:brightness=0.04,fps=10,tpad=stop_mode=clone:stop_duration=2.5,scale=1000:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=64[p];[b][p]paletteuse=dither=bayer" -loop 0 demo.gif && ls -lh demo.gif && ffmpeg -y -loglevel error -sseof -0.2 -i demo.gif -frames:v 1 $S/last.png && ffprobe -v error -show_entries format=duration -of csv=p=0 demo.gif
-rw-r--r--@ 1 macbookpro staff 667K Oct 30 22:54 demo.gif
28.200000
Shell cwd was reset to ~/dev/work-repo
<scratch>/last.png
[image]
The GIF is done: 28 seconds, 667 KB, and it ends on the full "stuck in a loop" whisper. I'll put a caption into the post's Demo section:
~/dev/haunted-shell/POST.md
The file ~/dev/haunted-shell/POST.md has been updated successfully.
The GIF is ready at ~/dev/haunted-shell/demo.gif: 28 seconds and 667 KB.
Why your command failed: the filename was there twice, so ffmpeg took at as the output file. On a second try, there was another catch: macOS writes a special narrow space before "PM" in screenshot names. A * wildcard in the path avoids both problems.
Changes from the plain conversion:
- Cropped to the terminal. In the full-screen capture the dim violet whispers were too small to read.
- Brightened a little so the whispers stand out against the dark background.
- Trimmed from 0:03 to 0:28.5, which drops the idle start and the clear at the end.
- The last frame holds for 2.5 seconds so viewers can read it before the loop restarts.
It shows git status failing, then "your git repo is dead, but i'm still here. i never learned to push." Then cd, and "why does it feel like we're both stuck in a loop?", which suits a GIF that loops. I added that as a caption in POST.md's Demo section.
To add it to the post, use the image upload in the DEV editor and replace the TODO line under Demo with the image it gives you.
The Spirit of It
While I was recording the demo, I cd'd into my day-job repo, and the ghost whispered: "you're still here. i'm still here. why does it feel like we're both stuck in a loop?" An 8B model running on my laptop, with no internet, called me out. Happy Halloween. 🕯️

Top comments (0)