Wake

Wake: Terminal History for Claude Code

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 can't see your terminal. It knows about commands it runs, but not the builds you kicked off, the errors you saw, the debugging you did. I wanted something that captured that 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 5MB of output get truncated by default—configurable if you really need more, but 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.

Data Management

Terminal history grows. Left unchecked, you end up with a database full of ancient build logs that nobody will ever look at again. Wake handles this automatically—sessions older than 21 days get pruned on each wake shell start. If you need manual control:

wake prune --dry-run      # see what would be deleted
wake prune --older-than 7 # delete sessions older than 7 days

Both retention and output limits are configurable via ~/.wake/config.toml:

[retention]
days = 21      # default

[output]
max_mb = 5     # default

Environment variables work too (WAKE_RETENTION_DAYS, WAKE_MAX_OUTPUT_MB) if you prefer that style.

What's Next

A few things I haven't built yet:

Smarter retrieval: Right now the MCP tools return full command output, which burns through context fast. The better approach is tiered retrieval—return lightweight metadata first (command, exit code, output size, summary), let Claude decide what's relevant, then fetch full output for just those commands. Good summaries require an LLM, which means either API calls (cost, privacy concerns) or bundling local inference. I'm leaning toward shipping a small model via candle (Rust-native ML) that downloads on first use. Keeps it self-contained—install once, summarization just works.

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.

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.