How oh-my-claudecode Manages State Across .omc/state/ and OMC_STATE_DIR
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 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.
// 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.
// 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.
// 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 suffix and scopes the file under the state/ subdirectory.
// 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.
// 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. 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.
// 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 (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 (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 (lines 61-78)
Practical Configuration Examples
Redirecting State to a Centralized Directory
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
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
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 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.
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 →