# kashmeister/prompt-controller architecture

> Architecture diagram of the GitHub repository kashmeister/prompt-controller, generated by GitDiagram from its file tree, README and a sample of source files (last updated 2026-06-23). It is an AI-made overview: check details against the source.

- Interactive diagram: https://gitdiagram.com/kashmeister/prompt-controller
- Repository: https://github.com/kashmeister/prompt-controller

## Overview

1. **Repository purpose**
   - This repo is a small macOS-only control layer around two existing agent CLIs: **Claude Code** and **Codex CLI**.
   - It does not implement an agent itself; it mediates between the human, the terminal app, and the underlying CLI binary to make approval prompts impossible to miss.

2. **Top-level shape**
   - There are two parallel wrapper stacks:
     - `claude-cli` → `claude_cli_wrapper.py`
     - `codex-cli` → `codex_cli_wrapper.py`
   - The duplication is intentional: each wrapper is tailored to the prompt style and state markers of its corresponding CLI, while keeping the same user-facing behavior and flags.

3. **Execution boundary**
   - The wrapper launches the real `claude` or `codex` binary inside a **pseudo-terminal (PTY)**.
   - All terminal I/O is passed through unchanged, so the underlying CLI still behaves like it was launched directly.
   - The wrapper sits “beside” the CLI stream, observing output but not replacing the agent.

4. **Prompt detection subsystem**
   - The core logic is a stream watcher that looks for blocking approval menus.
   - For **Codex**, it keys off the `[!] Action Required` title marker and its numbered approval menu.
   - For **Claude**, it matches the interactive approval menu rendered in terminal output, using a whitespace-insensitive comparison because Claude’s UI often uses cursor movement escapes rather than literal spaces.
   - It deliberately ignores normal chat output and completed turns; only blocking prompts trigger attention.

5. **Focus and notification subsystem**
   - When a prompt appears, the wrapper can:
     - write active prompt state to `/tmp`,
     - post a macOS notification,
     - and bring the agent’s terminal window to the front.
   - The focus behavior is careful: it targets only the relevant terminal window rather than globally raising every terminal window.
   - It also supports restoring the previously focused app afterward, depending on configuration.

6. **State-file contract**
   - Active prompt state is exposed as JSON at `/tmp/<cli>-cli-prompt-state.json`.
   - The file acts as the integration boundary for external tooling and for the wrapper’s own bookkeeping.
   - The README shows it containing fields like `active`, `terminalApp`, `savedFrontmostApp`, `excerpt`, and `pid`.
   - Stale state is cleared on startup, so the file is treated as ephemeral runtime state, not persistent config.

7. **Reminder loop / lifecycle**
   - While a prompt remains unanswered and the terminal is not frontmost, the wrapper periodically re-focuses and re-notifies.
   - The reminder cadence is separate from the initial detection event, so there is a continuous “nudging” loop until the agent resumes or the user responds.
   - Once the prompt ends, the wrapper clears state and stops the reminder behavior.

8. **Configuration model**
   - Both wrappers expose the same control flags:
     - `--no-notify`
     - `--no-activate`
     - `--no-restore`
     - `--state-file`
     - `--terminal-app`
     - `--debug`
     - `--debug-file`
   - Each wrapper also reads CLI-specific environment variables with distinct prefixes:
     - `CLAUDE_PROMPT_*`
     - `CODEX_PROMPT_*`
   - This creates a shared architecture with per-CLI namespacing rather than a single global config.

9. **Runtime and tooling constraints**
   - The project depends on **macOS**, **Python 3**, and the presence of the real `claude` / `codex` binaries on `PATH`.
   - Debug output is intentionally written to a file rather than the terminal, because the wrappers share the screen with a live TUI and terminal printing would corrupt rendering.
   - There is no server, database, or web frontend; the system is entirely local and process-driven.

10. **Optional input-control integration**
   - The `karabiner/prompt-controller-karabiner.json` file is a separate optional input layer for **Karabiner-Elements**.
   - It does not participate in the wrapper’s control flow; it only maps controller gestures to keystrokes for answering prompts faster.
   - This file belongs in a separate “human input enhancement” box in a diagram, distinct from the wrapper/CLI/terminal path.

11. **End-to-end data flow**
   - The key path is: user launches wrapper → wrapper spawns real CLI in PTY → CLI emits terminal output → wrapper detects approval prompt → wrapper updates `/tmp` state and notifies/focuses → user responds in the terminal → wrapper observes prompt clearance → wrapper clears state and optionally restores prior focus.
   - That is the main architecture boundary: **human ↔ wrapper ↔ terminal CLI**, with macOS notification/focus actions as side effects and `/tmp` state as the observable integration point.

Main entry points: [`claude_cli_wrapper.py`](https://github.com/kashmeister/prompt-controller/blob/main/claude_cli_wrapper.py) (Claude wrapper), [`codex_cli_wrapper.py`](https://github.com/kashmeister/prompt-controller/blob/main/codex_cli_wrapper.py) (Codex wrapper), [`karabiner/prompt-controller-karabiner.json`](https://github.com/kashmeister/prompt-controller/blob/main/karabiner/prompt-controller-karabiner.json) (Karabiner map)

## Diagram (Mermaid)

```mermaid
flowchart TD

subgraph group_group_wrappers["Prompt wrappers"]
  node_claude_wrapper["Claude wrapper<br/>python wrapper"]
  node_codex_wrapper["Codex wrapper<br/>python wrapper"]
  node_claude_watch["Claude watcher<br/>prompt detector"]
  node_codex_watch["Codex watcher<br/>prompt detector"]
end

subgraph group_group_runtime["Runtime boundary"]
  node_terminal["Terminal app<br/>macOS terminal"]
  node_pty["PTY bridge<br/>process I/O"]
  node_claude_bin["Claude Code<br/>agent CLI"]
  node_codex_bin["Codex CLI<br/>agent CLI"]
  node_state_file["/tmp state<br/>runtime state"]
  node_notify_focus["Notify & focus<br/>macOS actions"]
  node_reminder_loop(("Reminder loop<br/>lifecycle loop"))
  node_config_flags["Flags & env<br/>config"]
  node_debug_log["Debug file<br/>debug output"]
end

subgraph group_group_input["Human input enhancement"]
  node_karabiner["Karabiner map<br/>input remap"]
end

node_human(("Human<br/>operator"))

node_human -->|"launches"| node_claude_wrapper
node_human -->|"launches"| node_codex_wrapper
node_claude_wrapper -->|"spawns"| node_pty
node_codex_wrapper -->|"spawns"| node_pty
node_pty -->|"runs"| node_claude_bin
node_pty -->|"runs"| node_codex_bin
node_claude_bin -->|"renders output"| node_terminal
node_codex_bin -->|"renders output"| node_terminal
node_terminal -->|"stream seen by"| node_claude_watch
node_terminal -->|"stream seen by"| node_codex_watch
node_claude_watch -->|"updates"| node_state_file
node_codex_watch -->|"updates"| node_state_file
node_claude_watch -->|"triggers"| node_notify_focus
node_codex_watch -->|"triggers"| node_notify_focus
node_notify_focus -->|"feeds"| node_reminder_loop
node_reminder_loop -->|"repeats"| node_notify_focus
node_claude_wrapper -->|"reads"| node_config_flags
node_codex_wrapper -->|"reads"| node_config_flags
node_claude_wrapper -->|"writes"| node_debug_log
node_codex_wrapper -->|"writes"| node_debug_log
node_karabiner -.->|"assists"| node_human

click node_claude_wrapper "https://github.com/kashmeister/prompt-controller/blob/main/claude_cli_wrapper.py"
click node_codex_wrapper "https://github.com/kashmeister/prompt-controller/blob/main/codex_cli_wrapper.py"
click node_claude_watch "https://github.com/kashmeister/prompt-controller/blob/main/claude_cli_wrapper.py"
click node_codex_watch "https://github.com/kashmeister/prompt-controller/blob/main/codex_cli_wrapper.py"
click node_karabiner "https://github.com/kashmeister/prompt-controller/blob/main/karabiner/prompt-controller-karabiner.json"

classDef toneNeutral fill:#f8fafc,stroke:#334155,stroke-width:1.5px,color:#0f172a
classDef toneBlue fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#172554
classDef toneAmber fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef toneMint fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef toneRose fill:#ffe4e6,stroke:#e11d48,stroke-width:1.5px,color:#881337
classDef toneIndigo fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
classDef toneTeal fill:#ccfbf1,stroke:#0f766e,stroke-width:1.5px,color:#134e4a
class node_claude_wrapper,node_codex_wrapper,node_claude_watch,node_codex_watch toneBlue
class node_terminal,node_pty,node_claude_bin,node_codex_bin,node_state_file,node_notify_focus,node_reminder_loop,node_config_flags,node_debug_log toneAmber
class node_karabiner toneMint
class node_human toneNeutral
```

## Components

### Prompt wrappers

Parallel control layers for the two agent CLIs.

- [Claude wrapper](https://github.com/kashmeister/prompt-controller/blob/main/claude_cli_wrapper.py): python wrapper · MacOS control layer for Claude Code prompt detection and focus handling. · `claude_cli_wrapper.py`
- [Codex wrapper](https://github.com/kashmeister/prompt-controller/blob/main/codex_cli_wrapper.py): python wrapper · MacOS control layer for Codex CLI prompt detection and focus handling. · `codex_cli_wrapper.py`
- [Claude watcher](https://github.com/kashmeister/prompt-controller/blob/main/claude_cli_wrapper.py): prompt detector · Matches Claude approval menus using whitespace-insensitive stream comparison. · `claude_cli_wrapper.py`
- [Codex watcher](https://github.com/kashmeister/prompt-controller/blob/main/codex_cli_wrapper.py): prompt detector · Detects Codex blocking prompts via the action-required marker and numbered menu. · `codex_cli_wrapper.py`

### Runtime boundary

Local process and macOS integration around the wrappers.

- **Terminal app**: macOS terminal · Hosts the interactive CLI session and shows prompt menus.
- **PTY bridge**: process I/O · Pseudo-terminal boundary that preserves native terminal behavior.
- **Claude Code**: agent CLI · Real Claude binary launched by the wrapper inside a PTY.
- **Codex CLI**: agent CLI · Real Codex binary launched by the wrapper inside a PTY.
- **/tmp state**: runtime state · Ephemeral JSON prompt state used by the wrapper and external tooling.
- **Notify & focus**: macOS actions · Posts notifications, activates the terminal, and can restore prior focus.
- **Reminder loop**: lifecycle loop · Repeats notifications and refocuses while a prompt remains unanswered.
- **Flags & env**: config · Shared control flags and CLI-specific environment variables.
- **Debug file**: debug output · File-based debug logging to avoid corrupting live terminal rendering.

### Human input enhancement

Optional keyboard automation layer for faster prompt responses.

- [Karabiner map](https://github.com/kashmeister/prompt-controller/blob/main/karabiner/prompt-controller-karabiner.json): input remap · Optional key remapping layer for faster prompt responses. · `karabiner/prompt-controller-karabiner.json`

### Other components

- **Human**: operator · Launches wrappers, answers prompts, and resumes the agent.

## Connections

- Human → Claude wrapper: launches · User starts the Claude wrapper directly.
- Human → Codex wrapper: launches · User starts the Codex wrapper directly.
- Claude wrapper → PTY bridge: spawns · Wrapper creates a PTY-backed session for the real CLI.
- Codex wrapper → PTY bridge: spawns · Wrapper creates a PTY-backed session for the real CLI.
- PTY bridge → Claude Code: runs · Claude Code executes inside the pseudo-terminal.
- PTY bridge → Codex CLI: runs · Codex CLI executes inside the pseudo-terminal.
- Claude Code → Terminal app: renders output · Interactive output passes through to the terminal unchanged.
- Codex CLI → Terminal app: renders output · Interactive output passes through to the terminal unchanged.
- Terminal app → Claude watcher: stream seen by · Claude wrapper inspects terminal output for approval prompts.
- Terminal app → Codex watcher: stream seen by · Codex wrapper inspects terminal output for approval prompts.
- Claude watcher → /tmp state: updates · Writes active prompt state and clears it when the prompt ends.
- Codex watcher → /tmp state: updates · Writes active prompt state and clears it when the prompt ends.
- Claude watcher → Notify & focus: triggers · On prompt detection, notifies and brings the terminal forward.
- Codex watcher → Notify & focus: triggers · On prompt detection, notifies and brings the terminal forward.
- Notify & focus → Reminder loop: feeds · Continues nudging while the prompt remains unanswered and the terminal is not frontmost.
- Reminder loop → Notify & focus: repeats · Re-fires notifications and focus changes until the prompt clears.
- Claude wrapper → Flags & env: reads · Shared flags and CLAUDE_PROMPT_* environment settings.
- Codex wrapper → Flags & env: reads · Shared flags and CODEX_PROMPT_* environment settings.
- Claude wrapper → Debug file: writes · Debug output goes to a file instead of the live terminal.
- Codex wrapper → Debug file: writes · Debug output goes to a file instead of the live terminal.
- Karabiner map → Human: assists · Optional key remapping helps the human answer prompts faster.
