Skip to content
FunDev
FunDev
claude-code

AIForge: An Unattended Automation Workflow System for Claude Code

AIForge: An Unattended Automation Workflow System for Claude Code
11 views
12 min read
#claude-code

Could Claude Code Run Without a Human?

Claude Code CLI is powerful. Code reviews, document generation, and even bug fixes take just one terminal command. The drawback is that a person still has to enter that command every time.

What if it could check JIRA issues at 8 every morning, ask Claude to analyze any new ones, and stop on its own when token usage gets too high?

AIForge is the system I built to solve that problem: a web dashboard for running Claude Code CLI unattended through cron scheduling and JIRA issue polling.

GitHub: nasodev/claude-aiforge

What Kind of System Is It?

AIForge is a FastAPI-based web application with three core features.

FeatureDescription
Cron schedulingRun Claude Code automatically at specified times using APScheduler
JIRA issue pollingFind matching JIRA issues and process each automatically
Token managementMonitor claude.ai usage and pause automatically when limits are exceeded

Technology Stack

AreaTechnology
BackendPython 3.12+, FastAPI, Uvicorn
DatabaseSQLite (WAL mode), aiosqlite
SchedulerAPScheduler 3 (AsyncIO)
FrontendJinja2 SSR, Tailwind CSS (Dark theme)
Processpsutil (PID monitoring)
Port8010

Architecture

The system consists of four layers.

┌─ FastAPI Web Server (:8010)
│  ├─ Routes (Dashboard, Projects, Schedules, Executions, Settings, Logs)
│  ├─ Jinja2 Templates (SSR 다크 테마 UI)
│  └─ Static Assets (CSS, Icons)
│
├─ APScheduler (AsyncIO)
│  ├─ System Jobs (토큰 체크, 로그 모니터링)
│  └─ Project Schedules (크론 기반 실행)
│
├─ Services Layer
│  ├─ Executor (async/sync 모드 Claude CLI 실행)
│  ├─ Log Checker (PID 모니터링, 로그 파싱)
│  └─ Scheduler (Job 등록 및 생명주기 관리)
│
└─ SQLite Database (WAL mode)
   ├─ projects
   ├─ schedules
   ├─ executions
   └─ settings

The key design principle is using Claude CLI as a general-purpose executor. Instead of building a separate JIRA API client, it handles JIRA searches and tasks through Claude CLI and browser automation with --chrome.

Data Model

Four tables manage the entire system's state.

CREATE TABLE IF NOT EXISTS projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    type TEXT NOT NULL CHECK (type IN ('jira', 'schedule')),
    description TEXT,
    jira_search_conditions TEXT,  -- JIRA 타입일 때 검색 조건
    enabled INTEGER DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
CREATE TABLE IF NOT EXISTS schedules (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    prompt_template TEXT NOT NULL,  -- Claude에게 보낼 프롬프트
    work_dir TEXT NOT NULL,         -- Claude CLI 작업 디렉토리
    cron_expr TEXT NOT NULL,        -- 5-field 크론 표현식
    status TEXT DEFAULT 'idle'
        CHECK (status IN ('idle', 'running', 'paused', 'error')),
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
 
CREATE TABLE IF NOT EXISTS executions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    schedule_id INTEGER NOT NULL,
    pid INTEGER,               -- OS 프로세스 ID
    status TEXT DEFAULT 'running'
        CHECK (status IN ('running', 'success', 'error', 'timeout', 'killed')),
    command TEXT,               -- 실행된 CLI 명령어
    issue_key TEXT,             -- JIRA 이슈 키 (있는 경우)
    log_path TEXT,              -- Claude 로컬 로그 경로
    duration_seconds INTEGER,
    FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE CASCADE
);

The relationships are simple: Project → Schedules → Executions. CASCADE DELETE cleans up all child data when a project is deleted.

Two Execution Modes

AIForge runs Claude CLI in two modes, depending on the task.

Fire-and-Forget (Asynchronous Mode)

This is for project work. It launches Claude CLI as a subprocess, records only the PID, and returns immediately.

async def execute_async(
    prompt: str,
    work_dir: str,
    issue_key: str | None = None
) -> dict:
    """Fire-and-forget 실행. PID 기록 후 즉시 반환."""
    env = os.environ.copy()
    env.pop("CLAUDECODE", None)  # 재귀 실행 방지
 
    cmd = [
        "claude", "--chrome",
        "--dangerously-skip-permissions",
        "-p", prompt
    ]
 
    process = await asyncio.create_subprocess_exec(
        *cmd,
        cwd=work_dir,
        env=env,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
 
    return {"pid": process.pid, "status": "running"}

Removing the CLAUDECODE environment variable is essential. Otherwise, Claude CLI recognizes that it has been invoked inside itself, causing a recursive-execution problem.

Synchronous Mode

This is for system tasks whose results need immediate parsing, such as checking token usage and polling JIRA issues.

async def execute_sync(
    prompt: str,
    work_dir: str,
    timeout: int = 180
) -> dict:
    """동기 실행. 결과를 JSON으로 파싱하여 반환."""
    # ... subprocess 실행 ...
 
    stdout, stderr = await asyncio.wait_for(
        process.communicate(), timeout=timeout
    )
 
    # 3단계 JSON 파싱 폴백
    result = stdout.decode()
    try:
        return json.loads(result)           # 1. 순수 JSON
    except json.JSONDecodeError:
        # 2. ```json ... ``` 마크다운 블록에서 추출
        match = re.search(r'```json\s*(.*?)\s*```', result, re.DOTALL)
        if match:
            return json.loads(match.group(1))
        # 3. { ... } 객체 리터럴에서 추출
        match = re.search(r'\{.*\}', result, re.DOTALL)
        if match:
            return json.loads(match.group(0))

Claude's output is not always clean JSON, so extraction uses three fallbacks: plain JSON, a Markdown code block, and then an object literal.

JIRA Integration: Through the Browser, Without the API

JIRA integrations usually use its REST API. But authentication for the company's JIRA is complex, and configuring API permissions is cumbersome.

AIForge takes a different approach: manipulating JIRA directly through Claude CLI and Chrome browser automation.

1. 크론 트리거 → 스케줄 실행
2. 토큰 한도 체크
3. Claude 동기 실행: "다음 조건에 해당하는 JIRA 검색 해줘 {conditions}"
4. JSON 파싱 → 이슈 키 목록 추출
5. 이슈별로:
   - 이미 실행 중인지 확인
   - prompt_template에서 {issue_key} 치환
   - Claude CLI 비동기 실행
   - PID 기록
6. 백그라운드 로그 체커가 완료 모니터링

The JIRA search prompt is structured as follows:

poll_prompt = (
    f"다음 조건에 해당하는 JIRA 검색 해줘 {conditions} "
    "조회해서 json 으로 알려줘 출력에 다른값은 넣지 않고 json 만 넣어줘"
)

The format Claude returns:

[{"key": "FAS-130"}, {"key": "FAS-131"}]

The advantage is that JIRA API authentication does not need to be configured. If the browser is already logged in, that session is used as it is.

Token Management

Claude Code has usage limits. AIForge checks token usage periodically and automatically pauses work when it approaches a limit.

1. 시스템 잡 (기본 60분 간격)
2. Claude 동기 실행: "claude.ai/settings/usage 접속해서 사용량 확인"
3. 세션 사용률 / 주간 사용률 파싱
4. settings 테이블에 상태 저장
5. 한도 초과 시 → 모든 프로젝트 자동 일시정지

These settings are easy to change in the UI.

token_check_config = {
    "enabled": True,
    "interval_minutes": 60,
    "session_limit_percent": 80,   # 세션 사용량 80% 이상이면 정지
    "weekly_limit_percent": 90     # 주간 사용량 90% 이상이면 정지
}

Background Log Monitoring

How do we know when a fire-and-forget Claude CLI process has finished? A background log checker checks it periodically.

async def check_running_executions():
    """실행 중인 모든 작업의 상태를 확인한다."""
    running = await get_running_executions()
 
    for execution in running:
        pid = execution["pid"]
 
        # PID가 아직 살아있는지 확인
        if not psutil.pid_exists(pid):
            # 프로세스가 종료됨 → 로그 파일 분석
            log_content = await read_log_file(execution["log_path"])
 
            if has_error_pattern(log_content):
                await update_execution_status(execution["id"], "error")
            else:
                await update_execution_status(execution["id"], "success")
 
            # 실행 시간 계산 및 스케줄 상태 업데이트
            await update_schedule_status(execution["schedule_id"], "idle")

It uses psutil to check whether the PID exists. This is lighter than continuously polling stdout and can monitor hundreds of concurrent runs without difficulty.

The Settings System: Separating Config from Status

A key design choice is to divide settings data into two categories.

CategoryDescriptionExample
Config (user intent)Settings editable in the UItoken_check_config, log_monitor_config, global
Status (runtime state)State values updated automatically by the systemtoken_check_status, log_monitor_status
# Config 키 - 사용자가 변경
CONFIG_KEYS = ["token_check_config", "log_monitor_config", "global"]
 
# Status 키 - 시스템이 업데이트
STATUS_KEYS = ["token_check_status", "log_monitor_status"]

Changing Config automatically reloads APScheduler. For example, changing the token-check interval from 60 minutes to 30 removes the existing job and registers it again with the new interval.

Web Dashboard UI

The whole system is managed through a dark-themed, server-rendered web UI.

PageFeature
DashboardKPI statistics, token usage, and currently running tasks
ProjectsCreate, edit, and toggle projects (JIRA or schedule type)
SchedulesManage cron schedules and trigger manual runs
ExecutionsView execution history and filter by status
SettingsToken checks, log monitoring, and global settings
LogsGenerate claude-code-log HTML reports

Information visible at a glance on the dashboard:

  • Total project count and active status
  • Number of running tasks
  • Recent execution results (success/failure)
  • Token utilization (session/weekly)

From Startup to Execution: The Full Flow

When the server starts, initialization proceeds in this order.

run.py (uvicorn 엔트리)
  ↓
app.main:lifespan (FastAPI lifespan)
  ├─ init_db()
  │  ├─ schema.sql 읽기
  │  ├─ 테이블 생성 (없으면)
  │  └─ 기본 설정값 삽입
  │
  └─ init_scheduler()
     ├─ _register_system_jobs()
     │  ├─ 로그 모니터 (interval)
     │  └─ 토큰 체크 (interval)
     │
     └─ _register_project_schedules()
        └─ DB에서 활성 스케줄 로드
           └─ APScheduler 크론 잡으로 등록

A Practical Scenario: Automated Daily Code Reviews

  1. Create a project: type=schedule, name="daily-review"
  2. Set the schedule: cron=0 8 * * 1-5 (8 a.m. on weekdays)
  3. Write the prompt: “Review recent code changes and organize the findings by team”
  4. Working directory: /home/dev/project-alpha

At 8 a.m. on Monday:

  1. APScheduler triggers the job
  2. Token usage is checked synchronously
  3. Proceed if session usage is below 80% of the limit
  4. Run claude --chrome -p "최근 코드 변경사항을..." asynchronously
  5. Record the PID in the executions table
  6. Schedule status → running
  7. The log checker checks the PID every 10 minutes
  8. On completion → execution status=success; the schedule returns to idle

Automated JIRA Issue Processing

  1. Create a project: type=jira, search criteria="Project=FAS AND Status=Pending"
  2. Set the schedule: cron=*/30 * * * * (every 30 minutes)
  3. Prompt template: “Analyze issue {issue_key} and write a proposed solution as a comment”

Every 30 minutes:

  1. Claude searches JIRA for matching issues
  2. Return [{"key": "FAS-130"}, {"key": "FAS-131"}]
  3. Substitute each issue key into the prompt and run asynchronously
  4. Skip issues already being processed

Project Structure

aiforge/
├── run.py                    # uvicorn 엔트리포인트
├── schema.sql                # DB 스키마
├── requirements.txt          # Python 의존성
│
├── app/
│   ├── main.py               # FastAPI 앱, lifespan, 라우트 등록
│   ├── database.py           # SQLite 헬퍼, 설정 관리
│   │
│   ├── services/
│   │   ├── scheduler.py      # APScheduler, 실행 로직 (352 LOC)
│   │   ├── executor.py       # Claude CLI 서브프로세스 관리 (200 LOC)
│   │   └── log_checker.py    # PID 모니터링, 로그 파싱 (124 LOC)
│   │
│   ├── routes/
│   │   ├── dashboard.py      # 대시보드 통계
│   │   ├── projects.py       # 프로젝트 CRUD
│   │   ├── schedules.py      # 스케줄 CRUD + 수동 실행
│   │   ├── executions.py     # 실행 이력
│   │   ├── settings.py       # 설정 관리
│   │   ├── logs.py           # 로그 리포트
│   │   └── templates.py      # 워크스페이스 템플릿
│   │
│   └── templates/            # Jinja2 HTML 템플릿 (11개)
│
└── docs/
    ├── ARCHITECTURE.md
    └── PROJECT_ANALYSIS.md

The total codebase is about 1,600 LOC. The whole service fits into that amount of code because it uses Claude CLI as a general-purpose executor. There is no need to implement a separate JIRA client, web-scraping logic, and so on.

Design Decisions and Trade-offs

Why Use Claude CLI as a General-Purpose Executor?

Calling the JIRA REST API directly requires authentication setup (OAuth or API tokens) and response-parsing logic. Claude CLI with the --chrome flag can reuse the browser's existing logged-in session.

There are downsides. It is slower than an API, and Claude's responses are not always predictable. But the core goal of this automation system is unattended operation, not speed. An extra 10 seconds compared with an API is not a problem for a poll that runs once every 30 minutes.

PID-Based Status Tracking

Instead of watching stdout continuously, check only whether the PID exists.

  • Advantage: low resource consumption, even with hundreds of concurrent runs.
  • Disadvantage: no real-time progress information. Success or failure is determined only after completion.

Choosing SQLite

I chose SQLite instead of an external database such as PostgreSQL or Redis. WAL mode supports concurrent reads and writes, and there is no separate DB server to manage. SQLite is sufficient for a system running on one machine.

How to Run It

# 의존성 설치
pip install -r requirements.txt
 
# 실행
python run.py
# 또는
uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload

Open http://localhost:8010 in a browser to access the dashboard.

Cautions: Claude Code CLI must be installed on the system. Chrome is also required because the system uses the --chrome flag. It includes --dangerously-skip-permissions, so it should be used only on a trusted network.

Limitations and Future Plans

The current version is v0.1.0 and has several limitations.

LimitationDescription
No authenticationThe UI has no login (a trusted network is assumed)
Single machineNo distributed scheduling
Log parsingError-pattern matching is basic
No APIOnly HTML forms; no REST API endpoints
No webhooksNo external event triggers

These limitations are intentional, however. The core goal is unattended Claude Code automation on a single machine, and the current structure is sufficient for that purpose.

Wrapping Up

AIForge's core idea is simple: schedule Claude Code CLI like cron. I added JIRA issue polling, token management, and execution monitoring to create an unattended automation system.

All of this fits into 1,600 lines of Python because Claude CLI is already a powerful executor. JIRA searches, token-usage checks, and code reviews can all be delegated to Claude through prompts. AIForge is the wrapper that makes those instructions run automatically, repeatedly, and without a human present.

Automation in the AI era is less about writing complex code and more about sending the right instructions to AI at the right time. AIForge is my first attempt at that.

Related posts

Comments

Korean and English pages share this conversation.

Write a comment

0 / 5,000
You will need this password to edit or delete this comment.

Loading comments…