Clui CC โ Claude Code Desktop Overlay
Skill by ara.so โ Daily 2026 Skills collection.
Clui CC wraps the Claude Code CLI in a transparent, floating macOS overlay with multi-tab sessions, a permission approval UI (PreToolUse HTTP hooks), voice input via Whisper, conversation history, and a skills marketplace. It requires an authenticated claude CLI and runs entirely local โ no telemetry or cloud dependency.
Prerequisites
| Requirement |
Minimum |
Notes |
| macOS |
13+ |
Overlay is macOS-only |
| Node.js |
18+ |
LTS 20 or 22 recommended |
| Python |
3.10+ |
Needs setuptools on 3.12+ |
| Claude Code CLI |
any |
Must be authenticated |
| Whisper CLI |
any |
For voice input |
xcode-select --install
brew install node
node --version
python3 -m pip install --upgrade pip setuptools
npm install -g @anthropic-ai/claude-code
claude
brew install whisper-cli
Installation
Recommended: App installer (non-developer)
git clone https://github.com/lcoutodemos/clui-cc.git
On first launch macOS may block the unsigned app โ go to System Settings โ Privacy & Security โ Open Anyway.
Developer workflow
git clone https://github.com/lcoutodemos/clui-cc.git
cd clui-cc
npm install
npm run dev
Command scripts
./commands/setup.command
./commands/start.command
./commands/stop.command
npm run build
npm run dist
npm run doctor
Key Shortcuts
| Shortcut |
Action |
โฅ + Space |
Show / hide the overlay |
Cmd + Shift + K |
Fallback toggle (if โฅ+Space is claimed) |
Architecture
UI prompt โ Main process spawns claude -p โ NDJSON stream โ live render
โ tool call? โ permission UI โ approve/deny
Process flow
- Each tab spawns
claude -p --output-format stream-json as a subprocess.
RunManager parses NDJSON; EventNormalizer normalizes events.
ControlPlane manages tab lifecycle: connecting โ idle โ running โ completed/failed/dead.
- Tool permission requests arrive via HTTP hooks to
PermissionServer (localhost only).
- Renderer polls backend health every 1.5 s and reconciles tab state.
- Sessions resume with
--resume <session-id>.
Project structure
src/
โโโ main/
โ โโโ claude/ # ControlPlane, RunManager, EventNormalizer
โ โโโ hooks/ # PermissionServer (PreToolUse HTTP hooks)
โ โโโ marketplace/ # Plugin catalog fetch + install
โ โโโ skills/ # Skill auto-installer
โ โโโ index.ts # Window creation, IPC handlers, tray
โโโ renderer/
โ โโโ components/ # TabStrip, ConversationView, InputBar, โฆ
โ โโโ stores/ # Zustand session store
โ โโโ hooks/ # Event listeners, health reconciliation
โ โโโ theme.ts # Dual palette + CSS custom properties
โโโ preload/ # Secure IPC bridge (window.clui API)
โโโ shared/ # Canonical types, IPC channel definitions
IPC API (window.clui)
The preload bridge exposes window.clui in the renderer. Key methods:
window.clui.sendPrompt(tabId: string, text: string): Promise<void>
window.clui.resolvePermission(requestId: string, approved: boolean): Promise<void>
window.clui.createTab(): Promise<{ tabId: string }>
window.clui.resumeSession(tabId: string, sessionId: string): Promise<void>
window.clui.onTabEvent(tabId: string, callback: (event: NormalizedEvent) => void): () => void
window.clui.getHistory(): Promise<SessionMeta[]>
Working with Tabs and Sessions
Creating a tab and sending a prompt (renderer)
import { useEffect, useState } from 'react'
export function useClaudeTab() {
const [tabId, setTabId] = useState<string | null>(null)
const [messages, setMessages] = useState<NormalizedEvent[]>([])
useEffect(() => {
window.clui.createTab().then(({ tabId }) => {
setTabId(tabId)
const unsubscribe = window.clui.onTabEvent(tabId, (event) => {
setMessages((prev) => [...prev, event])
})
return unsubscribe
})
}, [])
const send = (text: string) => {
if (!tabId) return
window.clui.sendPrompt(tabId, text)
}
return { messages, send }
}
Resuming a past session
async function resumeLastSession() {
const history = await window.clui.getHistory()
if (history.length === 0) return
const { tabId } = await window.clui.createTab()
const lastSession = history[0]
await window.clui.resumeSession(tabId, lastSession.sessionId)
}
Permission Approval UI
Tool calls are intercepted by PermissionServer via PreToolUse HTTP hooks before execution. The renderer receives a permission_request event and must resolve it.
window.clui.onTabEvent(tabId, async (event) => {
if (event.type !== 'permission_request') return
const { requestId, toolName, toolInput } = event
const approved = await showApprovalDialog({ toolName, toolInput })
await window.clui.resolvePermission(requestId, approved)
})