Skip to content

Headless Mode Guide

Run Nova AI without interactive UI for CI/CD, automation, and batch processing.

Time: 15 minutes


What You'll Learn

  • Headless execution basics
  • Output formats (JSON, text)
  • Session management
  • GitHub Actions integration
  • Advanced automation patterns
  • Security best practices

When to Use Headless Mode

Scenario Use Headless? Why
GitHub Actions CI/CD Yes No terminal UI available
Cron jobs / scheduled tasks Yes Runs without interaction
Batch processing scripts Yes Programmatic control needed
IDE integration (Cursor) No Use /novaai command
Interactive development No Use /novaai command
One-off tasks No Interactive is faster

Quick Start

Basic Usage

# Simple headless execution
python scripts/nova_headless.py "add type hints to auth.py" \
  --output-format json

# With specific tool restrictions
python scripts/nova_headless.py "review code for security" \
  --output-format json \
  --allowed-tools "Read,Grep,Glob"

# With turn limit (cost control)
python scripts/nova_headless.py "analyze performance" \
  --output-format json \
  --max-turns 5

Direct Claude CLI

# True headless mode (no interactive UI)
claude --print "review authentication code" \
  --output-format json \
  --append-system-prompt "file:.claude/CLAUDE.md" \
  --permission-mode acceptAll \
  --no-interactive

CLI Reference

nova_headless.py

python scripts/nova_headless.py "<task>" [options]

Required: - <task> - Natural language task description

Options:

Flag Default Description
--output-format json Output format: json, text
--max-turns 10 Maximum conversation turns
--allowed-tools all Comma-separated tool list
--timeout 300 Timeout in seconds
--session-id new Resume existing session

Claude CLI Flags

Flag Description
--print Enable headless mode (no interactive UI)
--output-format json or text output
--permission-mode acceptAll for automation
--no-interactive Disable all prompts
--append-system-prompt Add custom instructions
--session-id Resume/continue a session

Output Formats

JSON Output

python scripts/nova_headless.py "review code" --output-format json

Response Structure:

{
  "success": true,
  "session_id": "session-abc123",
  "output": "Reviewed 5 files, found 2 issues...",
  "files_modified": [
    "src/auth.py",
    "tests/test_auth.py"
  ],
  "metrics": {
    "total_turns": 3,
    "input_tokens": 15420,
    "output_tokens": 2340,
    "total_cost_usd": 0.0523
  },
  "issues": [
    {
      "file": "src/auth.py",
      "line": 45,
      "severity": "high",
      "message": "SQL injection vulnerability"
    }
  ]
}

Text Output

python scripts/nova_headless.py "add logging" --output-format text

Response:

Task: add logging
Status: Success

Summary:
Added logging to 3 files with structured JSON output.

Files Modified:
- src/auth.py
- src/api.py
- src/utils.py

Cost: $0.0234


Session Management

Start New Session

# New session (default)
python scripts/nova_headless.py "implement feature X" \
  --output-format json > result.json

# Extract session ID for later
SESSION_ID=$(jq -r '.session_id' result.json)

Resume Session

# Continue previous work (saves 88-95% overhead)
python scripts/nova_headless.py "continue with tests" \
  --session-id "$SESSION_ID" \
  --output-format json

Multi-Turn Workflow

# Step 1: Implement
python scripts/nova_headless.py "implement auth endpoint" \
  --output-format json > step1.json
SESSION=$(jq -r '.session_id' step1.json)

# Step 2: Add tests (same session)
python scripts/nova_headless.py "add unit tests" \
  --session-id "$SESSION" \
  --output-format json > step2.json

# Step 3: Review (same session)
python scripts/nova_headless.py "review for security" \
  --session-id "$SESSION" \
  --output-format json > step3.json

GitHub Actions Integration

Basic Workflow

# .github/workflows/nova-ai.yml
name: Nova AI
on:
  issue_comment:
    types: [created]

jobs:
  nova-ai:
    if: contains(github.event.comment.body, '@nova-ai')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Dependencies
        run: pip install anthropic

      - name: Extract Command
        id: extract
        run: |
          COMMENT="${{ github.event.comment.body }}"
          COMMAND=$(echo "$COMMENT" | sed 's/@nova-ai //')
          echo "command=$COMMAND" >> $GITHUB_OUTPUT

      - name: Run Nova AI
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          python scripts/nova_headless.py "${{ steps.extract.outputs.command }}" \
            --output-format json > result.json

      - name: Post Result
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const result = JSON.parse(fs.readFileSync('result.json', 'utf8'));
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Nova AI Response\n\n${result.output}\n\n**Cost:** $${result.metrics.total_cost_usd.toFixed(4)}`
            });

PR Auto-Review

# .github/workflows/auto-review.yml
name: Auto PR Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get Changed Files
        id: files
        run: |
          FILES=$(git diff --name-only origin/main...HEAD | tr '\n' ' ')
          echo "changed=$FILES" >> $GITHUB_OUTPUT

      - name: Review Changes
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python scripts/nova_headless.py \
            "review these files for security and correctness: ${{ steps.files.outputs.changed }}" \
            --output-format json \
            --max-turns 10 > review.json

      - name: Post Review
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const review = JSON.parse(fs.readFileSync('review.json', 'utf8'));

            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
              event: review.issues?.length > 0 ? 'REQUEST_CHANGES' : 'APPROVE',
              body: review.output
            });

Nightly Security Scan

# .github/workflows/security-scan.yml
name: Nightly Security Scan
on:
  schedule:
    - cron: '0 2 * * *'  # 2 AM daily

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Security Scan
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python scripts/nova_headless.py \
            "comprehensive security audit of src/" \
            --output-format json \
            --max-turns 20 > security-report.json

      - name: Upload Report
        uses: actions/upload-artifact@v4
        with:
          name: security-report
          path: security-report.json

      - name: Create Issue if Vulnerabilities Found
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = JSON.parse(fs.readFileSync('security-report.json', 'utf8'));

            if (report.issues?.some(i => i.severity === 'high' || i.severity === 'critical')) {
              await github.rest.issues.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: '🚨 Security vulnerabilities detected',
                body: `## Security Scan Results\n\n${report.output}`,
                labels: ['security', 'priority:high']
              });
            }

Advanced Patterns

Batch Processing

#!/usr/bin/env python3
# scripts/batch_review.py
import asyncio
import json
from pathlib import Path
from src.orchestrator.claude_sdk_executor import ClaudeSDKExecutor

async def review_files(files: list[Path]) -> list[dict]:
    """Review multiple files in parallel."""
    executor = ClaudeSDKExecutor(
        project_root=Path.cwd(),
        agent_name="code-reviewer",
        model="claude-haiku-4-5-20251001"  # Cost-optimized
    )

    tasks = [
        executor.run_task(f"Review {file} for security issues")
        for file in files
    ]

    results = await asyncio.gather(*tasks, return_exceptions=True)
    return [r for r in results if not isinstance(r, Exception)]

if __name__ == "__main__":
    files = list(Path("src").glob("**/*.py"))
    results = asyncio.run(review_files(files))
    print(json.dumps(results, indent=2))

Pipeline Integration

#!/bin/bash
# scripts/ci-pipeline.sh

set -e

# Step 1: Lint
echo "Running lint..."
python scripts/nova_headless.py "run ruff check and fix issues" \
  --output-format json \
  --max-turns 5 > lint-result.json

# Step 2: Security review
echo "Running security review..."
python scripts/nova_headless.py "security audit of changed files" \
  --output-format json \
  --max-turns 10 > security-result.json

# Step 3: Generate report
echo "Generating report..."
python scripts/nova_headless.py "summarize lint and security results" \
  --output-format text > report.txt

# Check for failures
if jq -e '.issues | length > 0' security-result.json > /dev/null; then
  echo "Security issues found!"
  cat report.txt
  exit 1
fi

echo "Pipeline passed!"

Webhook Handler

# scripts/webhook_handler.py
from flask import Flask, request, jsonify
import subprocess
import json

app = Flask(__name__)

@app.route('/nova-ai', methods=['POST'])
def handle_webhook():
    """Handle incoming webhook requests."""
    data = request.json
    task = data.get('task', '')

    result = subprocess.run(
        ['python', 'scripts/nova_headless.py', task, '--output-format', 'json'],
        capture_output=True,
        text=True,
        timeout=300
    )

    return jsonify(json.loads(result.stdout))

if __name__ == '__main__':
    app.run(port=8080)

Security Best Practices

1. Never Expose API Keys

# CORRECT: Use secrets
env:
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

# WRONG: Hardcoded key
env:
  ANTHROPIC_API_KEY: sk-ant-xxxxx  # NEVER DO THIS

2. Redact Keys from Logs

- name: Run Nova AI
  run: |
    python scripts/nova_headless.py "$TASK" 2>&1 | \
      sed 's/sk-ant-[a-zA-Z0-9-]*/[REDACTED]/g'

3. Restrict Tool Access

# Read-only operations only
python scripts/nova_headless.py "analyze code" \
  --allowed-tools "Read,Grep,Glob"

# No Bash, Write, or Edit for untrusted inputs

4. Set Timeouts

# Prevent runaway processes
python scripts/nova_headless.py "task" \
  --timeout 300 \
  --max-turns 10

5. Rate Limiting

- name: Check Budget
  run: |
    DAILY_COST=$(curl -s $COST_TRACKER_URL)
    if [ "$DAILY_COST" -gt 100 ]; then
      echo "Daily budget exceeded"
      exit 1
    fi

6. Input Validation

# Sanitize user input before passing to Nova AI
import re

def sanitize_task(task: str) -> str:
    # Remove potential injection attempts
    task = re.sub(r'[;&|`$]', '', task)
    # Limit length
    task = task[:500]
    return task

Troubleshooting

Common Issues

Issue Solution
ANTHROPIC_API_KEY not set Export key: export ANTHROPIC_API_KEY=sk-ant-...
Timeout errors Increase --timeout or reduce task scope
Permission denied Use --permission-mode acceptAll
Session not found Don't use --session-id for new tasks
Rate limit exceeded Reduce concurrency or wait

Debug Mode

# Enable verbose logging
CLAUDE_DEBUG=1 python scripts/nova_headless.py "task" \
  --output-format json

# Check Claude CLI version
claude --version

Exit Codes

Code Meaning
0 Success
1 Failure (task failed)
2 Partial success
124 Timeout

Comparison: Interactive vs Headless

Feature /novaai (Interactive) Headless
Terminal UI Required Not needed
User prompts Yes (approval, questions) No (auto-approve)
Planning phase Full planning (~30s) No planning
KB search Automatic Optional
Quality gates All gates enforced Optional
Best for Development CI/CD, automation

What's Next?