# How Ponytail's Mode State Machine Manages Settings Across AI Agent Sessions

> Discover how Ponytail's mode state machine manages settings across AI agent sessions using environment variables, JSON config, session hooks, and filesystem flags.

- Repository: [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)
- Tags: internals
- Published: 2026-09-13

---

**Ponytail's mode state machine orchestrates runtime intensity levels through a three-stage architecture that resolves defaults via environment variables and JSON configuration, activates modes through session hooks, and persists changes via filesystem flag files in agent-specific directories.**

The **mode state machine** in [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail) governs how the AI coding assistant applies different rule intensities—from minimal (`off`) to maximum (`ultra`) enforcement—across Claude, Copilot, Qoder, and Codex environments. Unlike static configuration files, this state machine distinguishes between persistent user preferences, temporary session overrides, and transient review modes, ensuring that settings survive agent restarts while allowing rapid context switching during active development.

## Understanding the Mode State Machine Architecture

The state machine manages four distinct **runtime levels** (`off`, `lite`, `full`, `ultra`) alongside a temporary **review mode**. It separates concerns across three dedicated hooks in the `hooks/` directory:

1. **[`ponytail-config.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-config.js)** – Resolves the default mode from environment variables, JSON configuration, or hard-coded fallbacks.
2. **[`ponytail-activate.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-activate.js)** – Executes once per `SessionStart` event to initialize the active mode and emit the appropriate rule set.
3. **[`ponytail-mode-tracker.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-mode-tracker.js)** – Listens to `UserPromptSubmit` events to process user commands and update modes dynamically.

This separation ensures that **default configuration**, **session activation**, and **runtime mode changes** operate independently while maintaining consistency through a shared flag file mechanism.

## Default Mode Resolution Strategy

When a new session initializes, the mode state machine determines the starting intensity through a cascading resolution strategy implemented in [`hooks/ponytail-config.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-config.js):

### Environment Variable Priority

The system first checks for the `PONYTAIL_DEFAULT_MODE` environment variable. If set to a valid runtime level, this value takes precedence over all other configuration sources.

### Configuration File Fallback

If the environment variable is unset, the state machine searches for [`config.json`](https://github.com/DietrichGebert/ponytail/blob/main/config.json) in the following order:

- `$XDG_CONFIG_HOME/ponytail/config.json`
- `~/.config/ponytail/config.json` (Linux/macOS fallback)
- `%APPDATA%\ponytail\config.json` (Windows fallback)

The `defaultMode` field in this JSON file stores the user's persistent preference across sessions.

### Hard-Coded Default

If neither the environment variable nor the configuration file exists, the state machine defaults to `full` mode, ensuring consistent behavior for first-time users.

Only runtime levels (`off`, `lite`, `full`, `ultra`) are valid as defaults; the **review** mode is explicitly excluded from persistence and remains session-scoped only.

## Session Activation and Rule Injection

Upon every `SessionStart` event, [`hooks/ponytail-activate.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-activate.js) executes the activation sequence:

1. **Resolution**: Calls `getDefaultMode()` to determine the starting intensity.
2. **Flag Creation**: Invokes `setMode(mode)` to write the current mode to `.ponytail-active` in the agent-specific configuration directory.
3. **Rule Emission**: Calls `getPonytailInstructions(mode)` to filter and emit the appropriate rule subset to the AI agent.
4. **Status Nudging**: Prompts the user to configure a status-line badge if not already present.

If the resolved default is `off`, the hook immediately invokes `clearMode()` to remove any existing flag file and emits no rules, effectively deactivating Ponytail for the session.

## Runtime Mode Tracking and Command Processing

During active sessions, [`hooks/ponytail-mode-tracker.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-mode-tracker.js) monitors the `UserPromptSubmit` event to handle real-time mode transitions. This hook parses user input for specific command patterns:

- **`/ponytail default <mode>`** – Persists a new default via `writeDefaultMode(mode)`, updating the JSON configuration file for future sessions.
- **`/ponytail <mode>`** – Switches the session-scoped mode immediately via `setMode()`, writing to the flag file without modifying persistent defaults.
- **`/ponytail-review`** – Activates the temporary review mode for the current prompt only; does not persist or write to the flag file.
- **`/ponytail`** – Queries the current mode via `readMode()` and reports the active status.

The tracker also recognizes deactivation phrases (`stop ponytail`, `normal mode`) to trigger `clearMode()` and remove the active flag.

For **Qoder sessions**, which lack a `SessionStart` event, the tracker additionally injects the complete rule set on every prompt to ensure consistency.

## Persistence Mechanisms for Settings

The mode state machine employs a dual-persistence strategy to balance durability with performance.

### The Flag File System

The `.ponytail-active` file serves as the session state source of truth, stored in agent-specific directories:

- **Claude**: `~/.claude/`
- **Copilot**: `$COPILOT_PLUGIN_DATA/`
- **Qoder**: `~/.qoder/`

Three core functions manage this file:

- **`setMode(mode)`** – Uses `fs.writeFileSync` to atomically write the current mode to the flag file.
- **`readMode()`** – Retrieves the active mode from the flag file for rule filtering.
- **`clearMode()`** – Removes the flag file entirely, signaling deactivation.

### Default Configuration Storage

The `writeDefaultMode(mode)` function in [`hooks/ponytail-config.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-config.js) normalizes the input, creates the [`config.json`](https://github.com/DietrichGebert/ponytail/blob/main/config.json) directory structure if missing, and writes the validated mode to the `defaultMode` field. Subsequent sessions retrieve this value via `getDefaultMode()`, ensuring preferences survive agent restarts and system reboots.

## Practical Usage Examples

Interact with the mode state machine through natural chat commands:

```javascript
// Switch to ultra mode for the current session only
await agent.sendMessage('/ponytail ultra');

// Persist lite as the default for all future sessions
await agent.sendMessage('/ponytail default lite');

// Temporarily activate review mode without affecting defaults
await agent.sendMessage('/ponytail-review');

// Query current active mode
await agent.sendMessage('/ponytail');

// Deactivate Ponytail for this session
await agent.sendMessage('/ponytail off');

```

## Summary

- **Three-stage architecture** separates default resolution ([`ponytail-config.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-config.js)), session activation ([`ponytail-activate.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-activate.js)), and runtime tracking ([`ponytail-mode-tracker.js`](https://github.com/DietrichGebert/ponytail/blob/main/ponytail-mode-tracker.js)).
- **Cascading defaults** prioritize `PONYTAIL_DEFAULT_MODE` environment variables, then `~/.config/ponytail/config.json`, falling back to `full` mode.
- **Dual persistence** uses JSON files for permanent defaults and `.ponytail-active` flag files for session state.
- **Review mode remains transient** and never contaminates persistent configuration, while runtime levels (`off`, `lite`, `full`, `ultra`) support both temporary and persisted states.
- **Cross-agent support** adapts flag file locations for Claude, Copilot, Qoder, and Codex environments.

## Frequently Asked Questions

### How does Ponytail distinguish between temporary and persistent mode changes?

Session-scoped changes use the `/ponytail <mode>` command, which invokes `setMode()` to update the `.ponytail-active` flag file without touching the JSON configuration. Persistent changes require `/ponytail default <mode>`, which calls `writeDefaultMode()` to modify [`config.json`](https://github.com/DietrichGebert/ponytail/blob/main/config.json). The **review** mode activated via `/ponytail-review` bypasses both mechanisms entirely, existing only for the current prompt processing cycle.

### What happens if I set an invalid mode name?

The `writeDefaultMode()` function normalizes and validates input against the four runtime levels (`off`, `lite`, `full`, `ultra`). Invalid values are rejected before reaching the configuration file or flag file system, ensuring the state machine never enters an undefined state.

### Why does Ponytail use a flag file instead of environment variables for session tracking?

Environment variables cannot be modified dynamically within a running Node.js process without external shell manipulation. The `.ponytail-active` flag file provides a cross-platform, agent-agnostic mechanism that `readMode()` can check synchronously before every prompt, supporting real-time mode switches without requiring session restarts. This design also accommodates Qoder's lack of a `SessionStart` event by allowing rule injection on every prompt based on the current flag file contents.

### Where does Ponytail store my default mode preference across system restarts?

Persistent defaults are stored in [`config.json`](https://github.com/DietrichGebert/ponytail/blob/main/config.json) within the platform-specific configuration directory: `$XDG_CONFIG_HOME/ponytail/` on Linux, `~/.config/ponytail/` on macOS, or `%APPDATA%\ponytail\` on Windows. The `getDefaultMode()` function reads this file only during the `SessionStart` event, while `writeDefaultMode()` updates it when you issue `/ponytail default <mode>` commands.