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.
| Feature | Description |
|---|---|
| Cron scheduling | Run Claude Code automatically at specified times using APScheduler |
| JIRA issue polling | Find matching JIRA issues and process each automatically |
| Token management | Monitor claude.ai usage and pause automatically when limits are exceeded |
Technology Stack
| Area | Technology |
|---|---|
| Backend | Python 3.12+, FastAPI, Uvicorn |
| Database | SQLite (WAL mode), aiosqlite |
| Scheduler | APScheduler 3 (AsyncIO) |
| Frontend | Jinja2 SSR, Tailwind CSS (Dark theme) |
| Process | psutil (PID monitoring) |
| Port | 8010 |
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
└─ settingsThe 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.
| Category | Description | Example |
|---|---|---|
| Config (user intent) | Settings editable in the UI | token_check_config, log_monitor_config, global |
| Status (runtime state) | State values updated automatically by the system | token_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.
| Page | Feature |
|---|---|
| Dashboard | KPI statistics, token usage, and currently running tasks |
| Projects | Create, edit, and toggle projects (JIRA or schedule type) |
| Schedules | Manage cron schedules and trigger manual runs |
| Executions | View execution history and filter by status |
| Settings | Token checks, log monitoring, and global settings |
| Logs | Generate 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
- Create a project: type=schedule, name="daily-review"
- Set the schedule: cron=
0 8 * * 1-5(8 a.m. on weekdays) - Write the prompt: “Review recent code changes and organize the findings by team”
- Working directory:
/home/dev/project-alpha
At 8 a.m. on Monday:
- APScheduler triggers the job
- Token usage is checked synchronously
- Proceed if session usage is below 80% of the limit
- Run
claude --chrome -p "최근 코드 변경사항을..."asynchronously - Record the PID in the executions table
- Schedule status → running
- The log checker checks the PID every 10 minutes
- On completion → execution status=success; the schedule returns to idle
Automated JIRA Issue Processing
- Create a project: type=jira, search criteria="Project=FAS AND Status=Pending"
- Set the schedule: cron=
*/30 * * * *(every 30 minutes) - Prompt template: “Analyze issue
{issue_key}and write a proposed solution as a comment”
Every 30 minutes:
- Claude searches JIRA for matching issues
- Return
[{"key": "FAS-130"}, {"key": "FAS-131"}] - Substitute each issue key into the prompt and run asynchronously
- 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.mdThe 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 --reloadOpen 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.
| Limitation | Description |
|---|---|
| No authentication | The UI has no login (a trusted network is assumed) |
| Single machine | No distributed scheduling |
| Log parsing | Error-pattern matching is basic |
| No API | Only HTML forms; no REST API endpoints |
| No webhooks | No 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.





Comments
Korean and English pages share this conversation.
Loading comments…