ruvnet/ruflo · error · Error
policy administration requires an interactive local terminal
Error message
policy administration requires an interactive local terminal
What it means
requireInteractiveAdministrator() throws when stdin or stdout is not a TTY and the requested policy operation mutates state (init/migrate with a --mode or positional mode, rule add, budget set, revoke). Policy administration is deliberately gated to a local interactive terminal so CI jobs, piped stdin, and daemons cannot silently rewrite policy rules or budgets.
Solutions
- Run the mutating command in a real interactive terminal (local shell or interactive ssh)
- For scripted environments, mutate the policy state via the policy-runtime library API instead of the CLI
- Note plain `ruflo policy init` without --mode does NOT require a TTY — only mode changes and rule/budget/revoke mutations do
- As a last resort wrap in a pty (script -qec 'ruflo policy init --mode enforce' /dev/null), but prefer the API route
Example fix
# before (CI, non-TTY) ruflo policy init --mode enforce # after (plain migrate is allowed non-interactively) ruflo policy init # no --mode; set the mode later from an interactive terminal
Defensive patterns
Strategy: validation
Validate before calling
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
const needsTty = ['init-with-mode', 'rule add', 'budget set', 'revoke'];
if (needsTty.includes(op) && !isInteractive) {
// fall back to the policy-runtime library API instead of the CLI
} Type guard
function isInteractiveTerminal(): boolean {
return process.stdin.isTTY === true && process.stdout.isTTY === true;
} Try / catch
try {
await runPolicyCli(['init', '--mode', mode]);
} catch (err) {
if (err instanceof Error && err.message.includes('interactive local terminal')) {
// surface to the user to run manually, or use the library API from a TTY session
} else throw err;
} Prevention
- Design CI pipelines to never mutate policy state via the CLI
- Split automation into non-TTY-safe reads (status/audit/verify) and interactive writes
- Use docker exec -it / script -qec when a human must run mutations through a wrapper
When it happens
Trigger: Running `ruflo policy init --mode enforce` in CI, under nohup, from a Docker exec without -t, or piping input: echo ... | ruflo policy rule add '{...}'. Also triggered when only one of stdin/stdout is redirected.
Common situations: Trying to automate policy setup in Dockerfiles or CI pipelines; running through ssh with redirected stdio; testing via a shell script with stdin from a file.
Related errors
- approval issuance requires an authenticated human identity…
- contains shell metacharacters
- escapes project directory
- must be valid JSON
- requires a JSON argument
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/732f792262d13e71.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/commands/policy.ts:33
setPolicyMode,
upsertPolicyRule,
verifyPolicyLedger,
} from '../services/policy-runtime.js';
function argJson<T>(value: string | undefined, label: string): T {
if (!value) throw new Error(`${label} requires a JSON argument`);
try { return JSON.parse(value) as T; }
catch { throw new Error(`${label} must be valid JSON`); }
}
function print(data: unknown): CommandResult {
output.writeln(JSON.stringify(data, null, 2));
return { success: true, exitCode: 0, data };
}
function requireInteractiveAdministrator(): void {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
throw new Error('policy administration requires an interactive local terminal');
}
}
export const policyCommand: Command = {
name: 'policy',
description: 'Agentic policy engine — evaluate actions, manage rules/approvals, and verify the decision ledger (ADR-324)',
options: [
{ name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },
{ name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },
],
async action(context: CommandContext): Promise<CommandResult> {
const args = (context as { args?: string[] }).args ?? [];
const flags = (context as { flags?: Record<string, unknown> }).flags ?? {};
const root = String(flags.projectRoot ?? process.cwd());
const operation = args[0] ?? 'status';
try {
if (operation === 'init' || operation === 'migrate') {
if (flags.mode || args[1]) requireInteractiveAdministrator();View on GitHub (pinned to fa13ee4ad6)