Wake

Building Wake: A Terminal Recorder for the AI Coding Era

I built wake, a tool that records your terminal sessions so Claude Code can see what you've been doing. The idea is simple: instead of copy-pasting error messages or explaining "I tried X and Y but neither worked," your AI assistant just... knows.

The Problem

Claude Code is stateless between sessions. Every time you start a conversation, you're back to square one—re-explaining your project, what you've tried, what broke. The workaround is maintaining a CLAUDE.md file with context, but that's manual and tedious. I wanted something that captured context automatically.

The Design Decision: PTY vs Shell Hooks

The first real architecture question was how to capture terminal activity. Two approaches:

Shell hooks only (the simple path): zsh and bash have preexec/precmd hooks that fire before and after each command. Easy to implement—just append to your .zshrc:

preexec() { log_command "$1" }
precmd() { log_exit_code $? }

The problem: you get what was typed and whether it succeeded, but not what happened. The shell doesn't see stdout/stderr—those flow directly from child processes to your terminal, bypassing the shell entirely.

PTY wrapper (the complete path): Spawn the user's shell inside a pseudo-terminal. You sit in the middle, see every byte flowing in both directions, log it, pass it through. The user's experience is unchanged, but you capture everything.

I went with the PTY approach. Knowing that cargo test failed is marginally useful. Knowing which tests failed and why is actually useful.

The Hybrid Architecture

Pure PTY capture has its own problem: you get a raw byte stream with no structure. Where does one command end and the next begin? You can pattern-match on prompts, but that's fragile across different shell configurations.

The solution: use both. Shell hooks provide the framing (command start, command end, exit code), and the PTY provides the content (what actually got printed). The hooks communicate with the main wake process over a Unix socket:

┌─────────────────────────────────────────────────────────────────┐
│  wake shell                                                     │
│                                                                 │
│  PTY layer (captures all bytes, attributes to current command)  │
│                          ▲                                      │
│                          │ "command X started/ended"            │
│  Shell hooks ───────────────────────► Unix socket               │
│  (preexec/precmd)                                               │
└─────────────────────────────────────────────────────────────────┘

When preexec fires, wake knows a command is starting and begins buffering output. When precmd fires, wake closes out that command with its exit code and duration, writes everything to SQLite, and starts fresh.

Implementation Notes

I built this in Rust using portable-pty for cross-platform PTY handling. The crate abstracts the differences between Linux (/dev/pts/*) and macOS (/dev/ttys*), which was one less thing to worry about.

The main loop is async (tokio), selecting on stdin, the PTY master fd, the Unix socket for hook messages, and signals. Signal handling matters more than I expected—you need to forward SIGWINCH (terminal resize) to the child, and handle SIGINT/SIGTSTP correctly so ctrl-c and ctrl-z work as expected.

Output storage required some decisions. I keep two versions: the raw bytes (with ANSI escape codes for colors, cursor movement, etc.) and a stripped plaintext version for search and AI consumption. Commands with over 1MB of output get truncated—nobody needs their entire npm install log in their context window.

MCP Integration

The real payoff is the MCP server. Claude Code supports Model Context Protocol, which lets you expose tools that Claude can call. Wake's MCP server provides:

  • get_recent_sessions — list recent terminal sessions
  • get_session_commands — commands from a specific session
  • search_commands — search across all history

Now when you ask Claude "why did my deploy fail?", it can pull the relevant terminal output directly instead of you having to explain.

What's Missing

A few things I haven't built yet:

Summarization: The database grows. Ideally, older sessions would get compressed into summaries—"yesterday Joe was debugging a Redis connection issue, tried X and Y, eventually fixed it by Z." This probably requires periodic LLM calls, which adds complexity around cost and where to run the model.

Editor integration: Terminal is only half the picture. File changes, what you had open, what you were looking at—all relevant context that's currently not captured. Filesystem watching via inotify/FSEvents is straightforward, but deciding what's signal versus noise is the hard part.

Browser history: When you're debugging, you're probably also googling error messages, reading docs, looking at Stack Overflow. That context matters. Chrome stores history in SQLite, so it's technically accessible, but there are privacy considerations to think through.

Try It

curl -sSf https://raw.githubusercontent.com/joemckenney/wake/main/install.sh | sh
eval "$(wake init zsh)"
wake shell

Then add the MCP server to Claude Code:

claude mcp add --transport stdio --scope user wake-mcp -- wake-mcp

The code is at github.com/joemckenney/wake. It's early, rough around some edges, but it works. Feedback welcome.