How FluidVoice Implements Command Mode and Executes Terminal Commands Securely
FluidVoice's Command Mode uses a dual-service architecture where CommandModeService orchestrates LLM conversations and TerminalService executes sandboxed shell commands with mandatory safety checks, user confirmations for destructive operations, and JSON-encoded results.
FluidVoice is an open-source macOS application that transforms natural language into executable terminal commands through its agentic Command Mode. The implementation, found in the altic-dev/FluidVoice repository, combines a sophisticated conversation manager with a hardened execution environment to balance AI automation with system security. This article examines the exact mechanisms that allow the app to safely bridge LLM reasoning with real shell execution.
Architecture Overview
The implementation splits responsibilities between two core services. CommandModeService (Sources/Fluid/Services/CommandModeService.swift) manages the conversation state, builds LLM requests, parses tool calls, and enforces safety policies. TerminalService (Sources/Fluid/Services/TerminalService.swift) handles the actual process spawning, environment configuration, and result serialization. This separation ensures that security-critical execution logic remains isolated from the UI and orchestration layers.
All operations run on the main actor (@MainActor) to maintain UI consistency, with terminal execution performed via background async tasks that do not block the interface.
The Command Mode Workflow
Processing User Input
The workflow begins in CommandModeService.processUserCommand(_:notifyInvalidRequest:). This method accepts a user's spoken or typed phrase, appends it to conversationHistory, and persists the interaction via ChatHistoryStore. The service then initiates the LLM call sequence.
// In a SwiftUI view (e.g., CommandModeView.swift)
@StateObject private var cmdService = CommandModeService()
Button("Run") {
Task {
await cmdService.processUserCommand("Delete the file ~/Downloads/tmp.txt")
}
}
LLM Integration and Tool Call Detection
Inside CommandModeService.callLLM(), the service combines a system prompt defining the agentic workflow with the conversation history and sends it via LLMClient. When the LLM returns a tool_calls entry named execute_terminal_command, the parser extracts three critical fields: the command string, an optional workingDirectory, and a purpose classification (checking, executing, or verifying).
// Inside CommandModeService.callLLM()
if let tc = response.toolCalls.first,
tc.name == "execute_terminal_command" {
let command = tc.getString("command") ?? ""
let workDir = tc.getOptionalString("workingDirectory")
let purpose = tc.getString("purpose")
// Safety and execution logic follows...
}
Safety Gating and User Confirmation
Before execution, CommandModeService.isDestructiveCommand(_:) scans the command against patterns of dangerous operations including rm, mv, sudo, kill, chmod, dd, and pipe chains like | rm or && rm. If the command matches a destructive pattern and SettingsStore.shared.commandModeConfirmBeforeExecute is enabled, the service stores a PendingCommand instance and halts execution pending user approval.
if SettingsStore.shared.commandModeConfirmBeforeExecute,
self.isDestructiveCommand(command) {
self.pendingCommand = PendingCommand(
id: tc.id,
command: command,
workingDirectory: workDir,
purpose: purpose)
// UI will show "⚠️ Confirmation needed …"
return
}
Terminal Execution and Result Handling
Once confirmed (or immediately for non-destructive commands), CommandModeService.executeCommand(_:workingDirectory:callId:purpose:) delegates to TerminalService.execute(command:workingDirectory:timeout:). This method launches /bin/zsh as the current user, sets the working directory, applies a configurable timeout (default 30 seconds), and returns a CommandResult. The service wraps this in EnhancedCommandResult, converts it to JSON via resultToJSON(_:), and appends it to the conversation history as a tool message, allowing the LLM to react to the output.
let result = await terminalService.execute(
command: "rm -i ~/Downloads/tmp.txt",
workingDirectory: nil,
timeout: 15)
let json = terminalService.resultToJSON(result)
// → {"success":true,"command":"rm -i …","output":"…","exitCode":0,…}
Security Measures
FluidVoice implements multiple layers of protection to prevent malicious or accidental system damage:
-
Command Injection Prevention: Only commands explicitly returned via the
execute_terminal_commandtool are executed. The tool definition inTerminalService.toolDefinitionforces a structured JSON format with a mandatorypurposefield, requiring the LLM to reason about the command's intent before generation. -
Destructive Operation Detection: The
isDestructiveCommand(_:)method maintains a whitelist of risky prefixes and patterns. When detected, execution requires explicit user confirmation through thePendingCommandflow, or blocks entirely if confirmations are disabled. -
Privilege Escalation Blocking: The shell process launches as the current user without elevated privileges. Any
sudoinvocation prompts for a password in the terminal (invisible to the app), causing the automation to fail safely rather than executing with root access. -
Environment Sanitization:
TerminalServiceexplicitly prepends common Homebrew paths (/opt/homebrew/bin:/usr/local/bin) to thePATHenvironment variable while inheriting the user's shell environment, ensuring predictable binary resolution without exposing sensitive system locations. -
Working Directory Confinement: If no working directory is specified, commands execute in the user's home directory rather than system directories, preventing accidental operations in privileged locations.
-
Resource Limits: A watchdog
timeoutTaskterminates processes exceeding the configured timeout (default 30 seconds), preventing resource exhaustion from hanging commands. -
Result JSON Safety:
TerminalService.resultToJSON(_:)guarantees valid JSON output by falling back to a minimal safe structure if encoding fails, ensuring the LLM receives well-formed data and preventing injection attacks through malformed output. -
UI Synchronization: Streaming text and status updates are throttled to approximately 60fps and mirrored to
NotchContentState, preventing UI race conditions while maintaining responsiveness.
Code Implementation Examples
Confirming a Destructive Command
When the system detects a destructive operation, the UI layer presents a confirmation dialog bound to the pending command state:
// UI action bound to a "Confirm" button
Button("Run Anyway") {
Task {
await cmdService.confirmAndExecute()
}
}
The confirmAndExecute() method clears pendingCommand and forwards the stored parameters to executeCommand, resuming the workflow only after explicit user approval.
Analytics and Observability
After each command run, CommandModeService.captureCommandRunCompleted(success:) records metrics including turn counts, tool call counts, and confirmation requirements, enabling performance monitoring and safety auditing.
Summary
- FluidVoice's Command Mode is implemented across
CommandModeServiceandTerminalService, separating orchestration from execution. - The workflow processes natural language through an LLM, extracts structured tool calls, validates commands against destructive patterns, and executes them in a sandboxed
/bin/zshenvironment. - Security relies on mandatory
purposefields, pattern-based dangerous command detection, user confirmation workflows, non-privileged process execution, and strict timeout controls. - Results are always JSON-encoded and conversation history persists via
ChatHistoryStore, enabling multi-turn agentic workflows. - All operations respect the
commandModeConfirmBeforeExecutesetting, giving users final authority over destructive operations.
Frequently Asked Questions
What is Command Mode in FluidVoice?
Command Mode is an agentic workflow that converts natural language requests into executable terminal commands through a multi-turn conversation with a large language model. When the LLM determines a terminal command is necessary, it returns a structured tool call that FluidVoice validates and executes in a controlled environment, feeding the results back into the conversation for further reasoning or completion.
How does FluidVoice prevent dangerous terminal commands?
The system employs a multi-layered approach: the isDestructiveCommand(_:) method in CommandModeService scans for dangerous patterns like rm, sudo, chmod, and pipe chains that could lead to data loss. When detected, the command is stored as a PendingCommand and requires explicit user confirmation before execution. Additionally, the app never runs shells with elevated privileges, and timeouts prevent runaway processes.
Can FluidVoice execute commands with sudo privileges?
No, FluidVoice intentionally prevents privilege escalation. The TerminalService launches /bin/zsh as the current user only. If a command attempts to use sudo, the shell prompts for a password in the terminal (which is not visible to or controllable by the app), causing the command to fail safely rather than executing with root access. This design ensures that automated workflows cannot accidentally or maliciously modify system-level configurations.
What happens if a terminal command hangs or takes too long?
Each command execution includes a configurable timeout mechanism (defaulting to 30 seconds) implemented via timeoutTask in TerminalService.execute(command:workingDirectory:timeout:). If a process exceeds this limit, the watchdog task terminates it automatically, returning a timeout error to the LLM context. This prevents resource exhaustion and allows the agent to either retry with a different approach or inform the user of the failure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →