Skip to main content
Warden supports multiple output formats for different use cases: interactive terminal output, structured JSONL logs, and GitHub integration.

Terminal Output

TTY Mode

When running in an interactive terminal, Warden uses colored, formatted output:
warden scan
 __    __              _
/ / /\ \ \__ _ _ __ __| | ___ _ __
\ \/  \/ / _` | '__/ _` |/ _ \ '_ \
 \  /\  / (_| | | | (_| |  __/ | | |
  \/  \/ \__,_|_|  \__,_|\___|_| |_|
v1.0.0

FILES  3 files · 8 chunks
  ~ src/auth.ts (3 chunks)
  + src/api.ts (2 chunks)
  ~ src/config.ts (3 chunks)

SUMMARY
5 findings: ● 2 high  ● 2 medium  ● 1 low
Analysis completed in 15.8s · 3.0k in / 680 out · $0.00

Plain Text Mode (CI)

In non-TTY environments (CI/CD), Warden outputs plain text with timestamps:
warden scan 2>&1
[2026-03-05T20:00:00.000Z] warden: Warden v1.0.0
[2026-03-05T20:00:00.123Z] warden: Found 3 changed files with 8 chunks
[2026-03-05T20:00:15.800Z] warden: Summary: 5 findings (2 high, 2 medium, 1 low)
[2026-03-05T20:00:15.800Z] warden: Usage: 3.0k input, 680 output, $0.00
[2026-03-05T20:00:15.800Z] warden: Total time: 15.8s

Verbosity Levels

src/cli/output/verbosity.ts
export enum Verbosity {
  Quiet = 0,    // Only final summary
  Normal = 1,   // Standard output
  Verbose = 2,  // Include progress details
  Debug = 3,    // Full diagnostic output
}

Quiet Mode

warden scan -q
5 findings (2 high, 2 medium, 1 low)

Verbose Mode

warden scan -v
[1/3] src/auth.ts
  [1/3] L15-42 → 2 findings
  [2/3] L78-95 → 0 findings
  [3/3] L120-150 → 1 finding
[2/3] src/api.ts
  [1/2] L25-60 → 1 finding
  [2/2] L88-110 → 0 findings

Debug Mode

warden scan -vv
[debug] Prompt size: system=2400 chars, user=1800 chars, total=4200 chars (~1050 tokens)
[debug] Extraction: found 2 findings via regex
[debug] Cache read: 1200 tokens, cache creation: 800 tokens

JSONL Output

JSON Lines format for programmatic consumption and log archival.

Enabling JSONL

warden scan --output=report.jsonl

Record Structure

Run Metadata

src/cli/output/jsonl.ts
export const JsonlRunMetadataSchema = z.object({
  timestamp: z.string().datetime(),
  durationMs: z.number().nonnegative(),
  cwd: z.string(),
  runId: z.string(),
  traceId: z.string().optional(),
  model: z.string().optional(),
  headSha: z.string().optional(),
});

Skill Record

{
  "run": {
    "timestamp": "2026-03-05T20:00:00.000Z",
    "durationMs": 15800,
    "cwd": "/home/user/project",
    "runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "traceId": "trace-123",
    "model": "claude-sonnet-4-20250514",
    "headSha": "abc123"
  },
  "skill": "security-scanning",
  "summary": "Found 2 potential security issues",
  "findings": [
    {
      "id": "sec-001",
      "severity": "high",
      "confidence": "high",
      "title": "Unvalidated user input",
      "description": "User input is used directly in SQL query without validation",
      "location": {
        "path": "src/auth.ts",
        "startLine": 42,
        "endLine": 45
      }
    }
  ],
  "usage": {
    "inputTokens": 3000,
    "outputTokens": 680,
    "cacheReadInputTokens": 1200,
    "cacheCreationInputTokens": 800,
    "costUSD": 0.0048
  },
  "auxiliaryUsage": {
    "extraction": { "inputTokens": 100, "outputTokens": 20, "costUSD": 0.0001 }
  },
  "files": [
    {
      "filename": "src/auth.ts",
      "findings": 2,
      "durationMs": 3200,
      "usage": { "inputTokens": 800, "outputTokens": 150, "costUSD": 0.0012 }
    }
  ],
  "skippedFiles": [
    { "filename": "package-lock.json", "reason": "pattern", "pattern": "**/package-lock.json" }
  ],
  "failedHunks": 0,
  "failedExtractions": 0
}

Summary Record

{
  "run": {
    "timestamp": "2026-03-05T20:00:00.000Z",
    "durationMs": 15800,
    "cwd": "/home/user/project",
    "runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  },
  "type": "summary",
  "totalFindings": 5,
  "bySeverity": { "high": 2, "medium": 2, "low": 1 },
  "usage": {
    "inputTokens": 3000,
    "outputTokens": 680,
    "costUSD": 0.0048
  },
  "totalSkippedFiles": 1,
  "auxiliaryUsage": {
    "extraction": { "inputTokens": 100, "outputTokens": 20, "costUSD": 0.0001 }
  }
}

JSONL Schema

src/cli/output/jsonl.ts
export const JsonlRecordSchema = z.object({
  run: JsonlRunMetadataSchema,
  skill: z.string(),
  summary: z.string(),
  findings: z.array(FindingSchema),
  metadata: z.record(z.string(), z.unknown()).optional(),
  model: z.string().optional(),
  durationMs: z.number().nonnegative().optional(),
  usage: UsageStatsSchema.optional(),
  auxiliaryUsage: AuxiliaryUsageMapSchema.optional(),
  files: z.array(JsonlFileRecordSchema).optional(),
  skippedFiles: z.array(SkippedFileSchema).optional(),
  failedHunks: z.number().int().nonnegative().optional(),
  failedExtractions: z.number().int().nonnegative().optional(),
});

Parsing JSONL

src/cli/output/jsonl.ts
export function parseJsonlReports(content: string): ParsedJsonlLog {
  const lines = content.trim().split('\n').filter((line) => line.trim());
  const reports: SkillReport[] = [];
  let runMetadata: JsonlRunMetadata | undefined;
  let totalDurationMs = 0;

  for (const line of lines) {
    const parsed = JSON.parse(line);

    if (parsed.type === 'summary') {
      const summary = JsonlSummaryRecordSchema.parse(parsed);
      runMetadata = summary.run;
      totalDurationMs = summary.run.durationMs;
      continue;
    }

    const record = JsonlRecordSchema.parse(parsed);
    reports.push({
      skill: record.skill,
      summary: record.summary,
      findings: record.findings,
      // ... other fields
    });
  }

  return { reports, runMetadata, totalDurationMs };
}

Log Files

Warden automatically saves JSONL logs to .warden/logs/:
src/cli/output/jsonl.ts
export function getRepoLogPath(repoRoot: string, runId: string, timestamp: Date = new Date()): string {
  const ts = timestamp.toISOString().replace(/[:.]/g, '-');
  return join(repoRoot, '.warden', 'logs', `${shortRunId(runId)}-${ts}.jsonl`);
}

Log File Naming

.warden/logs/
├── a1b2c3d4-2026-03-05T20-00-00-000Z.jsonl
├── b2c3d4e5-2026-03-05T21-30-15-123Z.jsonl
└── c3d4e5f6-2026-03-06T09-15-42-456Z.jsonl

Log Retention

Configure log cleanup in warden.toml:
warden.toml
[logs]
cleanup = "auto"  # or "ask", "never"
retentionDays = 30
src/config/schema.ts
export const LogsConfigSchema = z.object({
  /** How to handle expired log files: 'ask' (default, prompt in TTY), 'auto' (silently delete), 'never' (keep all) */
  cleanup: LogCleanupModeSchema.default('ask'),
  /** Number of days to retain log files before considering them expired. Default: 30 */
  retentionDays: z.number().int().positive().default(30),
});

GitHub Output

PR Comments

Warden renders findings as markdown comments on pull requests:
src/output/renderer.ts
export function renderSkillReport(report: SkillReport, options: RenderOptions = {}): RenderResult {
  const review = renderReview(sortedFindings, report, includeSuggestions, failOn, findingsForFailOn, requestChanges);
  const summaryComment = renderSummaryComment(report, sortedFindings, groupByFile, checkRunUrl, hiddenCount);
  return { review, summaryComment };
}

Review States

src/output/types.ts
export type ReviewState = 'CHANGES_REQUESTED' | 'APPROVED' | 'COMMENTED';

Inline Comments

src/output/renderer.ts
const comments: GitHubComment[] = findingsWithLocation.map((finding) => {
  let body = `**${escapeHtml(finding.title)}**\n\n${escapeHtml(finding.description)}`;

  if (includeSuggestions && finding.suggestedFix) {
    body += `\n\n${renderSuggestion(finding.suggestedFix.description, finding.suggestedFix.diff)}`;
  }

  return {
    body,
    path: location.path,
    line: location.endLine ?? location.startLine,
    side: 'RIGHT' as const,
  };
});

Formatting Utilities

Duration Formatting

src/cli/output/formatters.ts
export function formatDuration(ms: number): string {
  if (ms < 1000) {
    return `${Math.round(ms)}ms`;
  }
  const totalSeconds = ms / 1000;
  if (totalSeconds < 60) {
    return `${totalSeconds.toFixed(1)}s`;
  }
  let minutes = Math.floor(totalSeconds / 60);
  let seconds = Math.round(totalSeconds % 60);
  return `${minutes}m ${seconds}s`;
}

Severity Formatting

src/cli/output/formatters.ts
const SEVERITY_CONFIG: Record<Severity, { color: typeof chalk.red; symbol: string }> = {
  high: { color: chalk.red, symbol: figures.bullet },
  medium: { color: chalk.yellow, symbol: figures.bullet },
  low: { color: chalk.green, symbol: figures.bullet },
};

export function formatSeverityBadge(severity: Severity): string {
  const config = SEVERITY_CONFIG[severity];
  return `${config.color(config.symbol)} ${config.color(`(${severity})`)}`;
}

Best Practices

Parse JSONL output in CI/CD pipelines:
warden scan --output=report.jsonl
jq '.findings[] | select(.severity == "high")' report.jsonl
Keep JSONL logs for cost tracking and trend analysis:
# Find all high-severity findings from last week
find .warden/logs -name "*.jsonl" -mtime -7 -exec jq '.findings[] | select(.severity == "high")' {} \;
# CI: Quiet mode
warden scan -q

# Local: Verbose with debug
warden scan -vv
For large codebases, JSONL files can grow substantially. Use log rotation:
[logs]
retentionDays = 14
cleanup = "auto"

Build docs developers (and LLMs) love