ruvnet/ruflo · warning · Error
No active session state found at .claude-flow/sessions/curre
Error message
No active session state found at .claude-flow/sessions/current.json
What it means
The session_end MCP tool in @claude-flow/cli requires an active session to close out. It calls loadActiveSessionState(), which reads .claude-flow/sessions/current.json under the project working directory (v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts:552-554); if that file does not exist or cannot be parsed, the function returns null and the handler at line 2374 throws this error. The file is normally created by the session-start flow (hooks session_start / pre-task), so hitting it means no session was ever opened, or the state file was deleted, or the tool ran from a different working directory.
Source
Thrown at v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts:2374
// Session end hook - stops daemon
export const hooksSessionEnd: MCPTool = {
name: 'hooks_session-end',
description: 'End current session, stop daemon, and persist state Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.',
inputSchema: {
type: 'object',
properties: {
saveState: { type: 'boolean', description: 'Save session state' },
exportMetrics: { type: 'boolean', description: 'Export session metrics' },
stopDaemon: { type: 'boolean', description: 'Stop worker daemon (default: true)' },
},
},
handler: async (params: Record<string, unknown>) => {
const saveState = params.saveState !== false;
const shouldStopDaemon = params.stopDaemon !== false;
const session = loadActiveSessionState();
if (!session) {
throw new Error('No active session state found at .claude-flow/sessions/current.json');
}
const sessionId = session.id;
const endedAt = Date.now();
const duration = Math.max(0, endedAt - Date.parse(session.startedAt));
const activity = loadSessionActivity(session, endedAt);
const summary = buildSessionSummary(activity, duration);
// Stop daemon if enabled
let daemonStopped = false;
if (shouldStopDaemon) {
try {
const { stopDaemon } = await import('../services/worker-daemon.js');
await stopDaemon();
daemonStopped = true;
} catch {
// Daemon may not be running
}
}View on GitHub (pinned to 5234333c34)
Solutions
- Verify .claude-flow/sessions/current.json exists in the project root you are running from (`ls .claude-flow/sessions/`); if missing, run the session-start flow first (`npx claude-flow hooks session-start --session-id <id>` or the session_start MCP tool), then retry session-end.
- Confirm the working directory matches the one where the session started: getProjectCwd() resolves the path relatively, so re-run from the original project root (or set CLAUDE_FLOW_CONFIG / project cwd appropriately) so .claude-flow/sessions/current.json is visible.
- If the file was consumed by an earlier session-end or deleted by cleanup, simply treat the session as already closed: re-run session-start to open a new session instead of retrying session-end.
- If the file exists but is corrupt/empty (0 bytes, partial JSON), delete it and start a fresh session; inspect for concurrent writers (two agents sharing one worktree) that truncated it.
- Audit your hooks configuration (e.g. .claude/settings.json hooks or codex config) to ensure SessionStart and SessionEnd hooks are registered as a pair, so end is never invoked without a preceding successful start.
Example fix
// before: session_end invoked with no session started
const result = await mcp.callTool('session_end', { exportMetrics: true });
// -> Error: No active session state found at .claude-flow/sessions/current.json
// after: start the session first, then end it
await mcp.callTool('session_start', { sessionId: 'sess-123' });
// ... work ...
const result = await mcp.callTool('session_end', { exportMetrics: true, saveState: true }); Defensive patterns
Strategy: try-catch
Validate before calling
import { existsSync } from 'node:fs';
import { join } from 'node:path';
// Run before calling session_end
const sessionFile = join(process.cwd(), '.claude-flow', 'sessions', 'current.json');
if (!existsSync(sessionFile)) {
// No session to close: either start one or skip the end call
console.log('No active session; skipping session_end.');
} else {
await mcp.callTool('session_end', { exportMetrics: true });
} Type guard
import { readFileSync } from 'node:fs';
interface ActiveSessionState { id: string; startedAt: string }
function hasActiveSession(path: string): ActiveSessionState | null {
try {
const raw = JSON.parse(readFileSync(path, 'utf-8'));
if (typeof raw?.id === 'string' && typeof raw?.startedAt === 'string') return raw;
} catch { /* missing or unparsable */ }
return null;
} Try / catch
try {
await mcp.callTool('session_end', { exportMetrics: true });
} catch (err) {
if (err instanceof Error && err.message.includes('No active session state found')) {
// Benign: nothing to close (already ended or never started). Log and continue.
console.warn('session_end skipped: no active session.');
} else {
throw err; // real failure (daemon stop, metrics export) — surface it
}
} Prevention
- Always pair session_start with session_end in hook configurations; never register the end hook alone.
- Pin the working directory: run claude-flow commands from the project root that owns .claude-flow/, especially in CI where steps may execute in different cwds.
- Make session teardown idempotent in your wrapper: check for .claude-flow/sessions/current.json (or catch this specific message) before/instead of blindly calling session_end.
- Avoid sharing one worktree between concurrent agents writing session state; give each run its own checkout so current.json is not clobbered.
- Add .claude-flow/sessions/ to cleanup exclusion lists (git clean filters, Dockerignore) so state survives between start and end steps.
When it happens
Trigger: Calling the claude-flow session_end MCP tool (or `npx claude-flow hooks session-end`) without a prior successful session_start in the same project; running the tool from a cwd other than the one where the session started (getProjectCwd() resolves .claude-flow/sessions/current.json relative to project cwd); the state file having been removed by a cleanup step, git clean, or a previous session-end that already consumed it; a corrupted/unparsable current.json that makes loadActiveSessionState return null.
Common situations: Hook misconfiguration where only the SessionEnd hook is wired into the host (Claude Code / Codex) but SessionStart is not; running session-end twice in a row (the first run finalizes and the state is gone); switching between repo checkouts or worktrees so the relative .claude-flow path no longer contains the session; CI or container runs where the .claude-flow directory is not persisted between the start and end steps; version upgrades that changed the session-state location or schema, leaving an old setup writing to a path the new reader does not find.
Related errors
- Path traversal blocked: ${realResolved}
- Path traversal blocked: ${resolved}
- hexToBytes: odd-length hex string
- trajectory envelope not found: ${path}
- browser/eval: script must not be empty
AI-assisted analysis of ruvnet/ruflo@5234333c34 (2026-08-21).
Data as JSON: /api/errors/b336c79d5b066816.
Report an issue: GitHub.