# How oh-my-claudecode Manages State Across .omc/state/ and OMC_STATE_DIR

> Discover how oh-my-claudecode manages state using OMC_STATE_DIR and .omc/state/. Learn about deterministic paths and session isolation for enhanced control.

- Repository: [Bellman/oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode)
- Tags: internals
- Published: 2026-03-27

---

**Oh-my-claudecode uses the `OMC_STATE_DIR` environment variable to redirect state from the local `.omc/state/` directory to a centralized location, while `getOmcRoot()` and `getProjectIdentifier()` ensure deterministic project-specific paths and session isolation.**

The oh-my-claudecode (OMC) framework persists all runtime metadata—including mode lifecycles, session histories, and team coordination data—within a structured state system. By default, this state lives under a hidden `.omc/state/` directory inside your worktree, but the system supports seamless redirection to external storage via the `OMC_STATE_DIR` environment variable. Understanding how OMC resolves these paths is essential for configuring centralized development environments or shared state across multiple clones.

## How OMC Selects Between Local and Centralized Storage

The `getOmcRoot()` function in [`src/lib/worktree-paths.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/lib/worktree-paths.ts) serves as the primary gatekeeper for state location selection. When `OMC_STATE_DIR` is undefined, the function returns a local path relative to the Git worktree root. When the variable is present, it constructs a project-specific subdirectory within the centralized location.

```typescript
// src/lib/worktree-paths.ts (lines 62-71)
export function getOmcRoot(worktreeRoot?: string): string {
  const customDir = process.env.OMC_STATE_DIR;
  if (customDir) {
    const root = worktreeRoot || getWorktreeRoot() || process.cwd();
    const projectId = getProjectIdentifier(root);
    const centralizedPath = join(customDir, projectId);
    // Warns if both legacy and centralized directories exist
    return centralizedPath;
  }
  const root = worktreeRoot || getWorktreeRoot() || process.cwd();
  return join(root, '.omc');
}

```

**Setting `OMC_STATE_DIR=/home/user/.omc/shared`** causes all state operations to target `$OMC_STATE_DIR/<project-id>/` instead of the local `.omc/` folder. The function emits a one-time warning if both locations contain existing data, prompting users to migrate.

### Deterministic Project Identification

To prevent collisions when using centralized storage, `getProjectIdentifier()` generates a unique 16-character hash based on the Git remote URL. This ensures that different clones of the same repository map to identical state directories regardless of local path differences.

```typescript
// src/lib/worktree-paths.ts (lines 31-49)
export function getProjectIdentifier(worktreeRoot?: string): string {
  const root = worktreeRoot || getWorktreeRoot() || process.cwd();
  let source: string;
  try {
    const remoteUrl = execSync('git remote get-url origin', { 
      cwd: root, encoding: 'utf-8' 
    }).trim();
    source = remoteUrl || root;
  } catch {
    source = root;
  }
  const hash = createHash('sha256').update(source).digest('hex').slice(0, 16);
  const dirName = basename(root).replace(/[^a-zA-Z0-9_-]/g, '_');
  return `${dirName}-${hash}`;
}

```

If no Git remote exists, the function falls back to hashing the worktree path, ensuring deterministic behavior even in non-Git contexts.

## Path Resolution and Security Validation

All state file operations route through `resolveOmcPath()`, which enforces strict boundary checks to prevent path traversal attacks. The function normalizes relative paths and validates that the resolved absolute path remains within the designated OMC root.

```typescript
// src/lib/worktree-paths.ts (lines 95-107)
export function resolveOmcPath(relativePath: string, worktreeRoot?: string): string {
  validatePath(relativePath);
  const omcDir = getOmcRoot(worktreeRoot);
  const fullPath = normalize(resolve(omcDir, relativePath));
  const relativeToOmc = relative(omcDir, fullPath);
  if (relativeToOmc.startsWith('..') || relativeToOmc.startsWith(sep + '..')) {
    throw new Error(`Path escapes omc boundary: ${relativePath}`);
  }
  return fullPath;
}

```

For mode-specific state files, `resolveStatePath()` automatically appends the [`-state.json`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/-state.json) suffix and scopes the file under the `state/` subdirectory.

```typescript
// src/lib/worktree-paths.ts (lines 119-124)
export function resolveStatePath(stateName: string, worktreeRoot?: string): string {
  const normalizedName = stateName.endsWith('-state') ? stateName : `${stateName}-state`;
  return resolveOmcPath(`state/${normalizedName}.json`, worktreeRoot);
}

```

## Session-Isolated State Management

To support parallel execution without state collisions, OMC implements session-scoped storage. When a `session_id` is provided, the framework redirects state operations to a dedicated subdirectory.

```typescript
// src/lib/worktree-paths.ts (lines 496-511)
export function ensureSessionStateDir(sessionId: string, worktreeRoot?: string): string {
  const dir = join(getOmcRoot(worktreeRoot), 'state', 'sessions', sessionId);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  return dir;
}

export function resolveSessionStatePath(mode: string, sessionId: string, worktreeRoot?: string): string {
  return join(ensureSessionStateDir(sessionId, worktreeRoot), `${mode}-state.json`);
}

```

**Session isolation** ensures that concurrent runs of the same mode (e.g., multiple `ralph` or `team` sessions) maintain independent JSON files under `.omc/state/sessions/<session-id>/`, while legacy state remains accessible for backward compatibility.

## MCP State Tools Interface

The actual read/write operations are exposed through three Model Context Protocol (MCP) tools defined in [`src/tools/state-tools.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/tools/state-tools.ts). These tools abstract the path resolution logic and handle atomic writes.

### Writing State with state_write

The `state_write` tool accepts a mode name, optional session ID, and arbitrary key-value pairs. It constructs a JSON payload with metadata fields (`_meta`) and writes atomically to the resolved path.

```typescript
// Example invocation
await Skill("state_write", {
  mode: "ralph",
  active: true,
  iteration: 1,
  current_phase: "execution",
  session_id: "20240327-abc123"
});

```

*Source:* [`src/tools/state-tools.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/tools/state-tools.ts) (lines 21-45, 70-86)

### Reading State with state_read

When invoked without a `session_id`, `state_read` aggregates both the legacy state file and all active session files, producing a unified report. With a specific `session_id`, it targets only that session's isolated file.

*Source:* [`src/tools/state-tools.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/tools/state-tools.ts) (lines 97-130)

### Clearing State with state_clear

The `state_clear` tool removes state files and, for `team` mode specifically, prunes the entire `team/` runtime directory under `.omc/state/`. This enables complete cleanup of coordination artifacts.

*Source:* [`src/tools/state-tools.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/tools/state-tools.ts) (lines 61-78)

## Practical Configuration Examples

### Redirecting State to a Centralized Directory

```bash
export OMC_STATE_DIR=$HOME/.omc/central
cd /path/to/project
omc run

```

All subsequent state operations automatically target `$HOME/.omc/central/<project-id>/state/` instead of the local worktree.

### Manual State Access from Custom Scripts

```typescript
import { stateWriteTool } from "./src/tools/state-tools.js";

await stateWriteTool.handler({
  mode: "ultrawork",
  active: true,
  iteration: 1,
  max_iterations: 10,
  session_id: "20240327-demo",  // Optional: isolates to specific session
});

```

### Reading Aggregated Session Data

```typescript
import { stateReadTool } from "./src/tools/state-tools.js";

const result = await stateReadTool.handler({ mode: "team" });
// Returns combined view of legacy state plus all active sessions

```

## Summary

- **Oh-my-claudecode** stores runtime data under `.omc/state/` by default, or under `$OMC_STATE_DIR/<project-id>/` when the environment variable is set.
- **Path resolution** flows through `getOmcRoot()` → `getProjectIdentifier()` → `resolveOmcPath()`, ensuring deterministic and secure file locations.
- **Security boundaries** are enforced by `resolveOmcPath()`, which validates that all resolved paths remain within the OMC root directory.
- **Session isolation** is achieved via `resolveSessionStatePath()`, which scopes files to `.omc/state/sessions/<session-id>/`.
- **MCP tools** (`state_read`, `state_write`, `state_clear`) provide the primary interface for persistence operations, automatically handling directory creation and atomic writes.

## Frequently Asked Questions

### What happens if both .omc/state/ and OMC_STATE_DIR exist for the same project?

Oh-my-claudecode detects this collision in `getOmcRoot()` and emits a one-time warning to stderr. The system prioritizes the `OMC_STATE_DIR` location when the variable is set, but the warning alerts users to potential data fragmentation, recommending migration of legacy files to the centralized location.

### How does OMC ensure the same project always uses the same centralized directory?

The `getProjectIdentifier()` function in [`src/lib/worktree-paths.ts`](https://github.com/Yeachan-Heo/oh-my-claudecode/blob/main/src/lib/worktree-paths.ts) generates a SHA-256 hash of the Git remote URL (or worktree path as fallback). This 16-character hash combined with the sanitized directory name creates a deterministic identifier that remains consistent across different clones of the same repository.

### Can multiple sessions run simultaneously without state conflicts?

Yes. When a `session_id` is provided to `state_write` or `state_read`, the tools invoke `resolveSessionStatePath()`, which isolates state files under `state/sessions/<session-id>/`. Parallel sessions write to distinct JSON files, preventing race conditions while `state_read` (without a session ID) aggregates all active sessions for monitoring.

### Is it safe to manually edit files in .omc/state/ or the OMC_STATE_DIR directory?

While the JSON files are human-readable, manual edits risk corrupting the `_meta` timestamps or `active` flags that modes depend on for lifecycle management. Always use the MCP `state_write` tool for modifications, as it validates schema constraints and maintains atomic write consistency.