Feature Agent — Complete Setup & Configuration Guide
Using FA day to day? The full feature reference is the User Guide. Running it in production? See the Operations Runbook — systemd services, Cloudflare Access tunnel, health-watcher auto-heal, the self-build loop, and recovery playbook. Browsable docs site: run
npm run docs:buildto generate a static documentation site from these docs. Seedocs-site/README.mdfor build and self-hosting instructions, and the Operations Runbook §"Docs site — publishing" for the deploy runbook (Cloudflare Pages via CI, or self-hosting the static output). This guide covers everything you need to set up Feature Agent from scratch, including Claude Code configuration, agent permissions, model selection, and integrations.
Tenant/org scope: FA now models an internal tenant/org scope one level above projects — a single default org today, not yet user-configurable. The
tenant_idfield appears on project reads; multi-org management arrives with a future increment.
Table of Contents
- Prerequisites
- Installation
- Claude Code Setup
- Agent Configuration
- Permissions & Safety
- Model Selection
- WhatsApp Notifications (Twilio)
- GitHub Integration — incl. PR-merge tracking, manual PR creation, revisions
- GitLab Integration — draft MRs, merge detection, revisions on gitlab.com or self-hosted
- Bitbucket Cloud Integration — PRs, merge detection, revisions on bitbucket.org
- Spec-Kit Pipeline
- Workspace Confinement & Data Provisioning
- Inbound Triggers — receive webhooks from GitHub/GitLab/Linear/Sentry/Slack
- Webhook Configuration
- Users & Roles
- Protected Projects
- Self-Managed Development
- Diagnostic Tool
- Production Deployment
- Troubleshooting
Prerequisites
- Node.js 18+ — Download
- Git — for cloning target repos
- Claude Code CLI — the autonomous agent engine
Installation
# Clone the repository
git clone https://github.com/scottallan/featureagent.git
cd featureagent
# Install dependencies
npm install
# Create your environment file
cp .env.example .envEdit .env with your configuration (detailed in sections below).
# Start in development mode (hot-reload)
npm run dev
# Or build for production
npm run build
npm startClaude Code Setup
Feature Agent uses the Claude Code CLI to autonomously implement features. You need Claude Code installed and authenticated on the machine running Feature Agent.
Install Claude Code
# Install globally via npm
npm install -g @anthropic-ai/claude-code
# Verify installation
claude --versionAuthenticate
# Interactive login (opens browser)
claude login
# Or set API key directly
export ANTHROPIC_API_KEY=sk-ant-...Verify It Works
# Quick test — should return a response
claude --print "Say hello"Custom CLI Path
If Claude Code is installed in a non-standard location:
CLAUDE_CLI_PATH=/usr/local/bin/claudeAgent Configuration
Core Settings
# How often the agent checks for new work (milliseconds)
AGENT_POLL_INTERVAL_MS=30000 # Default: 30 seconds
# Maximum parallel feature implementations
AGENT_MAX_CONCURRENCY=3 # Default: 1
# Maximum agent turns per implementation session
AGENT_MAX_TURNS=100 # Default: 100Concurrency
AGENT_MAX_CONCURRENCY controls how many features the agent works on simultaneously. Consider:
- 1 — Safe default. Features are processed one at a time, in order.
- 2-3 — Good for machines with ample CPU/RAM. Each implementation clones a repo and runs Claude Code.
- 5+ — For dedicated servers. Each concurrent job uses significant resources.
Retry
AGENT_MAX_RETRIES=3 # Default: 3 (set to 0 to disable auto-retry)When a feature fails, the agent will automatically retry it up to AGENT_MAX_RETRIES times. Each retry resets the feature to queued, clears previous artifacts, and increments retry_count. You can also manually retry via the API (POST /api/features/:id/retry) or the dashboard's Retry button.
Spec-conformance reviewer auto-revise loop
REVIEWER_MAX_ROUNDS=2 # Default: 2 (set to 0 to disable auto-revise loop)When reviewer: spec_conformance is enabled, REVIEWER_MAX_ROUNDS controls how many automatic revise-and-re-review rounds FA drives before escalating to a human. With the default of 2, FA will auto-revise up to twice before leaving the feature at implemented with a standing REQUEST_CHANGES review. Set to 0 to disable the loop entirely (FA posts the verdict once and stops — the original post-and-stop behavior).
How the Agent Works
Understanding what happens when the agent processes a feature helps with debugging, monitoring, and configuring your environment.
The Agent Loop
Feature Agent runs a polling loop inside the Node.js server process (no separate daemon or container). Every AGENT_POLL_INTERVAL_MS milliseconds (default: 30s), the loop:
- Picks up features in
analyzingstatus and runs a single-shot Claude Code call to evaluate them - Picks up features in
queuedstatus (up toAGENT_MAX_CONCURRENCY) and starts implementation - Picks up
failedfeatures withretry_count < AGENT_MAX_RETRIESand re-queues them
What Happens During Implementation
Each feature job runs through these steps:
1. Clone git clone --depth 50 -b main <repo_url>
into: WORKSPACE_DIR/<feature-id>/
2. Branch git checkout -b feature/<slug>-<id>
3. Implement claude --print --model sonnet --max-turns 100 \
--dangerously-skip-permissions \
"<implementation prompt>"
(spawned as a child process in the workspace dir)
4. Test npm test
(runs the project's test suite)
5. Document claude --print "<doc prompt>"
(generates docs/features/<slug>.md)
6. Push git add -A && git commit && git push -u origin <branch>
7. PR (optional) Creates a draft PR via GitHub API
8. Cleanup rm -rf WORKSPACE_DIR/<feature-id>/The Claude Code CLI is spawned as a child process using Node's child_process.spawn(). It runs with:
--printmode (non-interactive, outputs text)--output-format text--modelset to yourAGENT_MODEL(default:sonnet)--max-turnsset to yourAGENT_MAX_TURNS(default: 100)--dangerously-skip-permissionsif configured- A 10-minute timeout per Claude Code invocation
- Working directory set to the cloned repo
The stdout/stderr from Claude Code is captured and stored in the feature's implementation_log field.
Where Code Lives
| Path | Purpose | Lifetime |
|---|---|---|
WORKSPACE_DIR/<feature-id>/ | Cloned repo for implementation | Temporary — deleted after job completes or fails |
data/featureagent.db | SQLite database with all features, projects, users | Persistent |
Default WORKSPACE_DIR is ./workspaces relative to the Feature Agent install directory.
Monitoring the Agent
Server logs — The Feature Agent server logs all agent activity to stdout:
[agent] Starting agent loop (poll: 30000ms, concurrency: 1)
[agent] Processing feature: Build chess game (a1b2c3d4) [1/1 slots]
[agent] Feature implemented: a1b2c3d4 on feature/build-chess-game-a1b2c3d4 (PR: https://...)Or on failure:
[agent] Implementation failed for a1b2c3d4: Claude Code exited with code 1: ...
[agent] Retrying failed feature: Build chess game (a1b2c3d4) attempt 2/3Dashboard — The header shows active jobs / max concurrency. Feature cards show real-time status, and the detail view shows the full implementation log and test results after completion.
API — Poll the status endpoint and feature detail:
# Agent status (active jobs, model, concurrency)
curl http://localhost:3100/api/status
# Feature detail (status, implementation_log, test_results, retry_count)
curl http://localhost:3100/api/features/:id \
-H "Authorization: Bearer $API_KEY"Watch the workspace — While a feature is being implemented, you can watch the workspace directory to see files being created in real-time:
# Watch for new files
watch -n 2 "find ./workspaces -type f | head -30"
# Or tail the workspace for a specific feature
ls ./workspaces/
# Then watch the active one:
watch -n 2 "ls -la ./workspaces/<feature-id>/"Watch Claude Code process — Since Claude Code runs as a child process, you can see it in your process list:
ps aux | grep claudePermissions & Safety
This is the most important configuration decision. It determines how much autonomy the Claude Code agent has when implementing features.
Default Mode (Recommended for Getting Started)
AGENT_PERMISSIONS=defaultIn default mode, Claude Code runs with standard permissions. The agent can:
- Read and write files in the cloned workspace
- Run commands (npm test, git, etc.)
The agent will prompt for permission on certain operations, which in a headless environment may cause the implementation to stall. For fully autonomous operation, see the next section.
Dangerously Skip Permissions (Full Autonomy)
AGENT_PERMISSIONS=dangerously-skip-permissionsWARNING: This flag gives Claude Code unrestricted access to execute any command without confirmation. Only use this when:
- The agent runs in an isolated environment (container, VM, dedicated server)
- You trust the feature descriptions being submitted
- You have proper network isolation (the agent won't access sensitive internal systems)
What --dangerously-skip-permissions enables:
- File read/write without confirmation
- Shell command execution without confirmation
- Package installation without confirmation
- Any tool use without user approval prompts
Security recommendations when using full autonomy:
Run in a container — Use Docker to isolate the agent:
bashdocker run -d \ -e AGENT_PERMISSIONS=dangerously-skip-permissions \ -e ANTHROPIC_API_KEY=sk-ant-... \ -v /var/featureagent/data:/app/data \ featureagentLimit network access — The agent only needs to reach:
api.anthropic.com(Claude API)github.com(repo clone + push)- Notification endpoints:
api.telegram.org,discord.com,hooks.slack.com,api.twilio.com,graph.facebook.com, SMTP host (depending on configured channels)
Use fine-grained GitHub tokens — Only grant access to specific repositories the agent should modify. Never use tokens with admin/org-level access.
Restrict workspace directory — Keep
WORKSPACE_DIRon a separate partition or volume.
Allowlisted Tools (Future)
Claude Code also supports granular tool permissions via --allowedTools. This is a middle ground:
# Example: allow file ops and bash, but not network
claude --allowedTools "Read,Write,Edit,Bash" --print "..."This is not yet configurable via Feature Agent's .env but can be added to the CLI args in src/services/agent-engine.ts.
Model Selection
AGENT_MODEL=sonnet # DefaultAvailable models:
| Model | Setting | Best For |
|---|---|---|
| Claude Sonnet 4.6 | sonnet | Balanced speed and quality. Good default for most features. |
| Claude Opus 4.6 | opus | Complex features requiring deep reasoning. Slower, higher cost. |
| Claude Haiku 4.5 | haiku | Simple features, fast turnaround. Lower cost. |
Recommendations
- Start with
sonnet— Good balance of capability and cost for most features - Use
opusfor — Large architectural changes, complex multi-file features, features requiring deep understanding of the codebase - Use
haikufor — Simple bug fixes, config changes, documentation updates
Max Turns
AGENT_MAX_TURNS=100Controls how many "turns" (tool calls) the agent can make per implementation. Higher values allow the agent to handle more complex features but increase cost and time.
- 50 — Simple features (add a config option, small UI change)
- 100 — Medium features (new API endpoint, component)
- 200+ — Complex features (new subsystem, large refactor)
Notification Channels
Feature Agent supports multiple notification channels, configured per-project. Channels are stored in the project's notification_channels JSON array.
Supported Channels
| Channel | Cost | Setup | Best For |
|---|---|---|---|
| Telegram | Free | 5 min | Best free option. No limits, no verification. |
| Discord | Free | 2 min | Teams already using Discord. |
| Slack | Free tier | 5 min | Teams already using Slack. |
| Paid (Twilio) or free tier (Meta) | 15-60 min | Direct mobile notifications. | |
| Free (SMTP) | 10 min | Universal fallback. |
Notification Rules
Each channel has a notify array controlling who receives messages:
| Status Change | Default Recipient |
|---|---|
→ awaiting_approval | PO |
→ clarification_needed | Submitter (or PO in po_approval mode) |
→ in_progress | Submitter |
→ implemented | Submitter + PO |
→ failed | Submitter + PO |
Set notifications_enabled: false on a project to disable all notifications.
Telegram Setup (Recommended — Free)
- Message @BotFather on Telegram
- Send
/newbot, follow prompts, get your bot token - Message your bot, then visit
https://api.telegram.org/bot<TOKEN>/getUpdatesto find your chat_id - Configure:
curl -X POST http://localhost:3100/api/projects \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"repo_url": "https://github.com/org/my-app.git",
"autonomy_mode": "auto_safe",
"notification_channels": [
{
"type": "telegram",
"bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v",
"chat_id": "987654321",
"notify": ["all"]
}
]
}'For group notifications, add the bot to a group and use the group's chat_id (negative number).
Discord Setup
- In your Discord server: Server Settings → Integrations → Webhooks → New Webhook
- Copy the webhook URL
- Configure:
{
"type": "discord",
"webhook_url": "https://discord.com/api/webhooks/123456/abcdef...",
"notify": ["all"]
}Discord notifications include rich embeds with color-coded status.
Slack Setup
- Create a Slack app at api.slack.com/apps
- Enable Incoming Webhooks, add to a channel
- Copy the webhook URL
- Configure:
{
"type": "slack",
"webhook_url": "https://hooks.slack.com/services/T00/B00/xxxx",
"notify": ["all"]
}WhatsApp Setup
Via Twilio:
{
"type": "whatsapp",
"provider": "twilio",
"account_sid": "ACxxxxxxxx",
"auth_token": "your_token",
"from_number": "whatsapp:+14155238886",
"po_number": "whatsapp:+15551234567",
"submitter_number": "whatsapp:+15559876543",
"notify": ["all"]
}Via Meta Cloud API (free tier — 1,000 conversations/month):
{
"type": "whatsapp",
"provider": "meta",
"access_token": "EAAxxxxxxx",
"phone_number_id": "123456789",
"po_number": "15551234567",
"submitter_number": "15559876543",
"notify": ["all"]
}Email Setup
{
"type": "email",
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "you@gmail.com",
"smtp_pass": "app-specific-password",
"from": "featureagent@example.com",
"po_address": "po@company.com",
"submitter_address": "dev@company.com",
"notify": ["all"]
}For Gmail, use an App Password (not your regular password).
Multiple Channels
You can combine any number of channels per project:
"notification_channels": [
{ "type": "telegram", "bot_token": "...", "chat_id": "...", "notify": ["all"] },
{ "type": "discord", "webhook_url": "...", "notify": ["po"] },
{ "type": "email", "smtp_host": "...", "po_address": "...", "notify": ["po", "submitter"] }
]GitHub Integration
Feature Agent can automatically create draft pull requests when a feature is implemented.
Setup
Create a GitHub Personal Access Token (or fine-grained token):
- Go to GitHub → Settings → Developer settings → Personal access tokens
- For fine-grained tokens, grant access to specific repos with:
- Contents: Read and write
- Pull requests: Read and write
Configure:
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Enable per-project:
# Via API
curl -X PATCH http://localhost:3100/api/projects/:id \
-H "Content-Type: application/json" \
-d '{"auto_create_pr": true}'
# Or via the dashboard when enrolling/editing a projectWhat the Agent Creates
When a feature is successfully implemented with auto_create_pr enabled:
- A draft pull request is created (not ready for review)
- PR title:
feat: <feature title> - PR body includes: feature description, test results, doc path, agent attribution
- The PR targets the project's
default_branch(or the feature'sbase_branchoverride)
Draft PRs allow your team to review, request changes, or merge at their discretion.
PR-Merge & Review Tracking
With GITHUB_TOKEN set, the agent polls open PRs for implemented features each tick (up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):
- Merged PR → the feature auto-transitions to
merged(a terminal state). - Review comments → the count is recorded and surfaced in the dashboard as a
!next to the PR, so you know there's feedback to address.
Polling is skipped entirely when GITHUB_TOKEN is unset.
GITHUB_TOKENis read once at startup. After rotating the token, restart the server or PR creation/polling will keep using the old (expired) token.
Manual PR Creation
If the automatic PR creation failed (e.g. an expired token at implementation time), the branch is already pushed — recover with a pure API call once the token is fixed:
curl -X POST http://localhost:3100/api/features/:id/create-pr \
-H "Authorization: Bearer fa_your_api_key"This opens a draft PR for an implemented feature that has a branch_name but no pr_url. Failures surface as 502 (unlike the auto-flow, which swallows errors).
Addressing Review Comments (Revisions)
After a PR is reviewed, nudge the agent to apply the requested changes on the same branch:
curl -X POST http://localhost:3100/api/features/:id/revise \
-H "Authorization: Bearer fa_your_api_key"This flips the feature to revising. The agent fetches the PR's review feedback (review summaries + inline code comments + conversation comments), re-clones the branch, applies the changes, runs tests, and pushes to the same branch — updating the existing PR (no new PR). It returns to implemented. If there are no comments, or on error, it reverts to implemented with the PR left intact.
GitLab Integration
Feature Agent supports GitLab-hosted projects natively. When a project's repo_url points to gitlab.com (or a self-hosted GitLab instance), FA automatically creates draft Merge Requests, polls for merges, surfaces MR review comments, and posts spec-conformance reviews — all without any per-project configuration beyond the credentials below.
Setup
Create a GitLab personal access token (or a project access token) with at minimum:
- api scope (needed for MR creation, notes, and approvals)
Configure:
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx- For self-hosted GitLab (optional):
# Hostname only — no https:// prefix, no trailing slash
GITLAB_HOST=gitlab.mycompany.comWhen GITLAB_HOST is set, any project whose repo_url matches that hostname is routed to the GitLab provider automatically.
How provider selection works
FA selects the VCS provider solely from the hostname in the project's repo_url (or pr_url). No per-project configuration is required:
| Host in URL | Provider used |
|---|---|
github.com | GitHub (Octokit, GITHUB_TOKEN) |
gitlab.com | GitLab (REST v4, GITLAB_TOKEN) |
value of GITLAB_HOST | GitLab (REST v4, GITLAB_TOKEN) |
| anything else | GitHub (backward-compatible default) |
What FA creates on GitLab
When a feature is implemented with auto_create_pr enabled on a GitLab-hosted project:
- A draft Merge Request is created (
Draft: feat: <title>) - MR description includes: feature description, test results, doc path, agent attribution
- The MR targets the project's
default_branch(or the feature'sbase_branchoverride)
MR-Merge & Review Tracking
With GITLAB_TOKEN set, FA polls open MRs for implemented features (same cadence as GitHub: up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):
- Merged MR → feature auto-transitions to
merged. - Review comments → count is recorded and surfaced in the dashboard as a
!next to the MR link.
Manual MR Creation & Revisions
The same /create-pr and /revise endpoints work for GitLab MRs:
# Recover a failed auto-MR
curl -X POST http://localhost:3100/api/features/:id/create-pr \
-H "Authorization: Bearer fa_your_api_key"
# Address MR review comments
curl -X POST http://localhost:3100/api/features/:id/revise \
-H "Authorization: Bearer fa_your_api_key"
GITLAB_TOKENis read once at startup. After rotating the token, restart the server.
Bitbucket Cloud Integration
Feature Agent supports Bitbucket Cloud (bitbucket.org) natively. When a project's repo_url points to bitbucket.org, FA automatically creates PRs, polls for merges, surfaces PR review comments, and posts spec-conformance reviews — all without any per-project configuration beyond the credentials below.
Bitbucket Server / Data Center is NOT supported. These products use a different REST API (v1.0,
/rest/api/1.0) and are out of scope. Onlybitbucket.org(Bitbucket Cloud) is supported.
Setup
Option A — App password (most common)
App passwords are per-user credentials that scope permissions without sharing your account password.
- Go to Bitbucket Cloud → Personal settings → App passwords → Create app password
- Name it (e.g.
feature-agent) and grant these scopes:- Repositories:
Read - Pull requests:
Write
- Repositories:
- Copy the generated password (shown once).
- Configure:
BITBUCKET_TOKEN=your-app-password
BITBUCKET_USERNAME=your-bitbucket-usernameWhen BITBUCKET_USERNAME is set, FA uses HTTP Basic auth (username:app-password) — the standard Bitbucket Cloud app-password flow.
Option B — Repository or workspace access token
If your Bitbucket workspace has workspace/repository access tokens enabled (Bitbucket Cloud Premium):
- Go to Workspace settings → Security → Access tokens (or Repository settings → Access tokens)
- Create a token with
repository:readandpullrequest:writescopes. - Configure:
BITBUCKET_TOKEN=your-access-token
# BITBUCKET_USERNAME — leave unset when using a workspace/repo access tokenWithout BITBUCKET_USERNAME, FA sends the token as Authorization: Bearer <token>.
How provider selection works
FA selects the VCS provider solely from the hostname in the project's repo_url (or pr_url). No per-project configuration is required:
| Host in URL | Provider used |
|---|---|
github.com | GitHub (Octokit, GITHUB_TOKEN) |
gitlab.com | GitLab (REST v4, GITLAB_TOKEN) |
value of GITLAB_HOST | GitLab (REST v4, GITLAB_TOKEN) |
bitbucket.org | Bitbucket Cloud (REST v2.0, BITBUCKET_TOKEN) |
| anything else | GitHub (backward-compatible default) |
Both HTTPS and SSH repo URLs are recognized:
https://bitbucket.org/myworkspace/myrepo.gitgit@bitbucket.org:myworkspace/myrepo.git
What FA creates on Bitbucket
When a feature is implemented with auto_create_pr enabled on a Bitbucket-hosted project:
- A PR is created with the title
Draft: feat: <title> - PR description includes: feature description, test results, doc path, agent attribution
- The PR targets the project's
default_branch(or the feature'sbase_branchoverride)
Draft PR limitation: Bitbucket Cloud's REST API does not expose a native draft PR flag. FA prefixes the title with
Draft:to signal intent — the same approach used for GitLab. Merge or close the PR as usual; FA detects the merge via polling.
PR-Merge & Review Tracking
With BITBUCKET_TOKEN set, FA polls open PRs for implemented features (same cadence as GitHub/GitLab: up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):
- Merged PR → feature auto-transitions to
merged. - Review comments → count is recorded and surfaced in the dashboard as a
!next to the PR link.
Manual PR Creation & Revisions
The same /create-pr and /revise endpoints work for Bitbucket PRs:
# Recover a failed auto-PR
curl -X POST http://localhost:3100/api/features/:id/create-pr \
-H "Authorization: Bearer fa_your_api_key"
# Address PR review comments
curl -X POST http://localhost:3100/api/features/:id/revise \
-H "Authorization: Bearer fa_your_api_key"
BITBUCKET_TOKENandBITBUCKET_USERNAMEare read once at startup. After rotating credentials, restart the server.
Spec-Kit Pipeline
Feature Agent integrates with GitHub Spec Kit for spec-driven development, enabled per project.
Enrolling a Project
# Clone, run `specify init --ai claude`, generate a constitution, open a bootstrap PR
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/enableStatus flows disabled → enrolling → awaiting_merge. Watch live enrollment logs:
curl http://localhost:3100/api/projects/:id/spec-kit/logs
# or in the dashboard log viewer: ?id=enroll-<projectId>After merging the bootstrap PR, finalize:
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/mark-enabled # → 'enabled'Already have Spec Kit configured? Skip enrollment — detection promotes the project straight to enabled if .specify/ and a constitution both exist:
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/checkTo turn it off: POST /api/projects/:id/spec-kit/disable.
Running a Feature Through Spec-Kit
Submit a feature with "use_spec_kit": true (silently coerced to false if the project isn't enabled). The agent runs the pipeline in order:
/speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.analyze → /speckit.implement
It pauses at clarification gates and resumes from the saved spec/plan/task files once the questions are answered — the same clarification flow as auto_safe mode.
Workspace Confinement & Data Provisioning
Each feature runs in an isolated workspace (workspaces/<feature_id>/), and the agent is instructed to stay inside it and ignore absolute paths in repo docs. To give features the external data they legitimately need without letting the agent reach outside its workspace, declare it on the project:
curl -X PATCH http://localhost:3100/api/projects/:id \
-H "Content-Type: application/json" \
-d '{
"data_dirs": ["/data/signal_cache", {"src": "/data/fixtures", "dest": "test/fixtures"}],
"setup_command": "npm ci"
}'After cloning each workspace, the agent (provisionWorkspace()):
- Validates
data_dirsexist (step one). Every declared source must exist on the agent host; a missing one fails the run up front with an actionable error instead of silently continuing. - Runs
git lfs pull(best-effort — fine if the repo has no LFS). - Symlinks each
data_dirsentry read-only into the workspace and adds it to.git/info/excludeso it's never committed (path-traversal guarded). - Runs
setup_commandin the workspace (a non-zero exit fails the run).
data_dirs entries are either a path string ("/abs/src" — destination defaults to the basename) or { "src": "/abs/src", "dest": "rel/path" }.
Declared Gate Commands (gates: in .fa/environment.yml)
Declare an ordered list of shell commands in the committed .fa/environment.yml that FA runs inside the sandbox after implementation, after tests, and after the verify step — and before creating any commit or PR. FA reads only the exit code and captured output; it never interprets what the command does (§VII).
# .fa/environment.yml
image: node:20
test: npm test
gates:
# Blocking gate (default: block: true) — non-zero exit fails the run,
# no commit pushed, no PR opened. Workspace preserved for inspection.
- name: lint
command: npm run lint
# Advisory gate — non-zero exit recorded as a warning, run continues.
- name: advisory-scan
command: ./my-scanner.sh
block: falseGate execution:
- FA runs each gate via
bash -lc "<command> 2>&1"inside the sandbox Runtime (the same isolated environment the agent used). IfRUNTIME=docker, gates run inside the container. - Gates run in order. All gates execute (even after a blocking failure) so the full picture is captured before failing.
- A failing blocking gate transitions the feature to
failedand preserves the workspace. No commit is pushed and no PR is created. - Advisory gate failures (
block: false) are recorded aswarnbut do not block.
Gate shape:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | — | Non-empty label used in logs, run-events, and the provenance artifact. |
command | string | yes | — | Shell command to run. FA runs it and reads only the exit code. |
block | boolean | no | true | true = blocking (fail the run on non-zero); false = advisory (warn, continue). |
Unknown keys or malformed gate entries throw ManifestValidationError — the run aborts before any agent spend.
Observability: every gate result is recorded as a gate_result run-event (visible in the dashboard's run-events timeline) and committed into the provenance artifact (.fa/provenance/<id>.md).
No gates declared → strict no-op. A project with no gates: key behaves byte-identically to before — no extra cost, no behavior change.
Inbound Triggers
Feature Agent can receive webhooks from external tools (GitHub, GitLab, Linear, Sentry, Slack) and automatically create features from them. Each incoming event enters the project's standard autonomy-mode funnel — a po_approval project still requires human sign-off before any agent spend.
Enabling
Set the environment variable before starting the server:
FA_INBOUND_TRIGGERS_ENABLED=trueWithout this flag the ingest routes return 404. The discovery endpoint (GET /api/projects/:id/triggers) still returns the provider list with inbound_enabled: false so you can preview what URLs will be active.
Finding your webhook URLs
Open the dashboard, locate your project in the project table, and click Triggers. The modal shows:
- An enabled/disabled indicator reflecting
FA_INBOUND_TRIGGERS_ENABLED. - A list of all registered providers (GitHub, GitLab, Linear, Sentry, Slack) with their full absolute webhook URLs — copy them directly into the provider's webhook settings.
- A signing-secret hint: set the provider's webhook secret to this project's API key.
You can also query the endpoint directly:
curl http://localhost:3100/api/projects/<projectId>/triggers \
-H "Authorization: Bearer <project-api-key>"Response:
{
"inbound_enabled": true,
"providers": [
{ "key": "github", "webhook_path": "/api/projects/<id>/triggers/github" },
{ "key": "gitlab", "webhook_path": "/api/projects/<id>/triggers/gitlab" },
{ "key": "linear", "webhook_path": "/api/projects/<id>/triggers/linear" },
{ "key": "sentry", "webhook_path": "/api/projects/<id>/triggers/sentry" },
{ "key": "slack", "webhook_path": "/api/projects/<id>/triggers/slack" }
]
}Per-provider webhook setup
The signing secret for every provider is the project API key (fa_...). Paste it into the provider's "webhook secret" or "signing secret" field.
GitHub — Settings → Webhooks → Add webhook
- Payload URL:
https://your-fa-host/api/projects/<id>/triggers/github - Content type:
application/json - Secret: your project API key
- Events: Issues → "Let me select individual events" → check Issues
GitLab — Settings → Webhooks → Add new webhook
- URL:
https://your-fa-host/api/projects/<id>/triggers/gitlab - Secret token: your project API key
- Trigger: check Issues events
Linear — Settings → API → Webhooks → New webhook
- URL:
https://your-fa-host/api/projects/<id>/triggers/linear - Secret: your project API key
- Resources: check Issues
Sentry — Settings → Integrations → Webhooks → Add to Project
- Webhook URL:
https://your-fa-host/api/projects/<id>/triggers/sentry - Secret: your project API key
- Events: check issue (issue-alert created)
Slack — Use a Slack workflow or custom app that POSTs a JSON body:
{
"title": "Feature title",
"description": "Optional details",
"event_id": "<unique-id>",
"channel": "#general",
"user": "@alice"
}- URL:
https://your-fa-host/api/projects/<id>/triggers/slack - Set the
x-fa-tokenheader to your project API key (or include"token": "..."in the body)
Webhook Configuration
Feature Agent fires HTTP POST webhooks to your project's callback_url on every status change.
Setup
Set callback_url when enrolling a project:
curl -X POST http://localhost:3100/api/projects \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"repo_url": "...",
"autonomy_mode": "auto_safe",
"callback_url": "https://my-app.com/api/webhooks/featureagent"
}'Payload Format
{
"event": "feature.status_changed",
"feature_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"title": "Dark mode support",
"old_status": "queued",
"new_status": "in_progress",
"branch_name": "feature/dark-mode-support-550e8400",
"pr_url": null,
"timestamp": "2025-01-15T10:30:00.000Z"
}Security
Webhooks include an HMAC-SHA256 signature in the X-FeatureAgent-Signature header, using your project's API key as the secret:
X-FeatureAgent-Signature: sha256=abc123...Verify on your end:
const crypto = require('crypto');
const expectedSig = 'sha256=' + crypto
.createHmac('sha256', projectApiKey)
.update(JSON.stringify(requestBody))
.digest('hex');
if (receivedSignature !== expectedSig) {
return res.status(401).send('Invalid signature');
}Retry Policy
- 3 retries with exponential backoff (1s, 2s, 4s)
- Configurable via
WEBHOOK_MAX_RETRIESandWEBHOOK_TIMEOUT_MS - Non-2xx responses trigger a retry
Local Email + Password Login
FA supports native email/password login as a second human-login method (alongside Google and API keys). It uses the same session cookie infrastructure as Google Sign-In.
Requirements
SESSION_SECRETmust be set (already required by Google login — same secret).- SMTP configuration (
SMTP_HOSTetc.) is needed for password-reset emails; without it, the endpoint still returns 200 and logs a warning (graceful degradation).
Environment variables
# Required for any human login (Google or local)
SESSION_SECRET=<at-least-32-random-bytes-hex>
# Optional: SMTP for password-reset/invite emails
SMTP_HOST=smtp.example.com
SMTP_PORT=587 # default 587; use 465 for TLS
SMTP_USER=notify@example.com
SMTP_PASS=<app-password>
SMTP_FROM=Feature Agent <notify@example.com>
# Base URL used in reset-link emails (default: http://localhost:3100)
APP_BASE_URL=https://your-fa-domain.comGenerate a strong SESSION_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Inviting a user (setting a first password)
Local login is invite-only — users cannot self-register. The operator creates the account, then the user sets their own password via the reset flow:
Create the user account (admin API):
bashcurl -s -X POST https://your-fa-domain.com/api/users \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alice@company.com", "name": "Alice", "role": "admin"}'Send Alice the login page URL:
https://your-fa-domain.com/login.htmlAlice clicks "Set / forgot password?", enters her email, and receives a reset link by email (if SMTP is configured). She clicks the link and sets her password.
Without SMTP: Alice can't receive the email automatically. You can instead generate the reset link manually via
POST /auth/local/reset-request(which logs a warning and returns 200) and then extract the token from server logs or use the API — or simply set the password directly via the API in a dev environment.
How it works
- Passwords are hashed with scrypt (N=16384, r=8, p=1, 32-byte output, random per-user salt). The stored form is self-describing:
scrypt$N$r$p$<salt>$<hash>. - Constant-time compare prevents timing attacks. The login handler always runs scrypt regardless of whether the email exists, keeping timing consistent (prevents user enumeration).
- Password reset tokens are stateless HMACs over
{userId, exp, fingerprint}wherefingerprintis derived from the currentpassword_hash. Setting a new password invalidates all prior tokens (single-use). - Tokens expire after 24 hours.
- Failed logins are tracked in-memory per email+IP. After 5 consecutive failures, the account+IP is locked out for 15 minutes (configurable via
LOCAL_AUTH_MAX_ATTEMPTSandLOCAL_AUTH_LOCKOUT_MS).
API endpoints
| Endpoint | Method | Body | Description |
|---|---|---|---|
/auth/local/login | POST | {email, password} | Issue a session cookie. 401 on wrong credentials (uniform, no enumeration). |
/auth/local/reset-request | POST | {email} | Send reset email. Always returns 200 (no enumeration). |
/auth/local/reset | POST | {token, new_password} | Set password with a valid single-use token. |
Configuring Google OAuth
Google Sign-In lets human users log into the FA dashboard with their Google account instead of pasting a raw API key. This is optional — API-key auth always works regardless.
Prerequisites
- A Google Cloud project with the OAuth 2.0 API enabled.
- An OAuth 2.0 Client ID of type "Web application".
Step-by-step
- Go to Google Cloud Console → APIs & Services → Credentials.
- Click "Create Credentials" → "OAuth 2.0 Client ID". Choose "Web application".
- Under "Authorized redirect URIs", add your redirect URI:
- Development:
http://localhost:3100/auth/google/callback - Production:
https://your-fa-domain.com/auth/google/callback
- Development:
- Click Create. Note the Client ID and Client Secret.
Environment variables
Add these three variables to your .env (or system environment — never commit secrets):
# Google OAuth (for human sign-in to the dashboard)
GOOGLE_CLIENT_ID=123456789-xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=https://your-fa-domain.com/auth/google/callback
# Session secret — signs the browser session cookie (generate a strong random value)
SESSION_SECRET=<at-least-32-random-bytes-hex-or-base64>SESSION_SECRET must be set for Google sign-in (or API-key-to-session conversion) to work. If it is absent, Bearer-key auth still works but the "Sign in with Google" button returns a configuration error. Generate a strong value with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"How it works
- FA implements OAuth 2.0 authorization code + PKCE (RFC 7636) with
statefor CSRF protection on the handshake. - The returned
id_tokenis verified locally (signature against Google's JWKS,aud,iss,exp,email_verified). No secret is ever logged. - Sign-in is invite-only: the verified Google email must match a pre-created FA user (
POST /api/users). Unknown emails are rejected with a clear message. No auto-provisioning for self-hosted. - On success, FA issues an httpOnly + SameSite=Lax (+
Securein production) signed session cookie valid for 7 days. State-changing requests via the session cookie require anX-CSRF-Tokenheader (the dashboard handles this automatically).
Redirect URI in production
Set GOOGLE_REDIRECT_URI to the exact URI registered in Google Cloud Console, including the scheme and path. If your FA instance is behind a reverse proxy (Nginx, Cloudflare Tunnel), use the public-facing HTTPS URL.
User provisioning
Before a Google user can sign in, an admin must create their FA user account with a matching email:
curl -s -X POST https://your-fa-domain.com/api/users \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alice@company.com", "name": "Alice", "role": "admin"}'Alice can then sign in with Google using alice@company.com.
SSO / OIDC — Enterprise Identity (Generic IdP)
FA supports a bring-your-own-IdP SSO option using standards-based OpenID Connect (OIDC). Any OIDC-compliant issuer works — Okta, Azure AD / Entra, Auth0, Keycloak, PingFederate, Google Workspace (via generic OIDC), or any OpenID Connect–compliant provider. This is distinct from the Google-specific integration: the generic OIDC path uses OIDC Discovery (/.well-known/openid-configuration) and is configured entirely by env vars.
Requirements
SESSION_SECRETmust be set (same secret used by Google and local-password login).- An OAuth 2.0 / OIDC application registered at your IdP with the authorization code flow enabled.
- The IdP must issue an
id_tokenwithemailandemail_verifiedclaims (standard OIDC scopeopenid email profile).
Step-by-step (generic)
- Create an OIDC application (called "Web application", "Regular Web App", or similar) in your IdP's admin console.
- Set the Allowed Redirect URI / Callback URL to:
- Development:
http://localhost:3100/auth/oidc/callback - Production:
https://your-fa-domain.com/auth/oidc/callback
- Development:
- Note the Client ID and Client Secret issued by the IdP.
- Note the Issuer URL (e.g.
https://login.microsoftonline.com/<tenant-id>/v2.0for Entra ID, orhttps://your-org.okta.comfor Okta). FA appends/.well-known/openid-configurationto this URL for discovery.
Environment variables
# Generic OIDC/SSO (enterprise sign-in — spec 059 inc 2)
OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=<your-client-id>
OIDC_CLIENT_SECRET=<your-client-secret> # secret — never commit to source control
OIDC_REDIRECT_URI=https://your-fa-domain.com/auth/oidc/callback
# Optional:
OIDC_SCOPES=openid email profile # default — change only if your IdP needs extra scopes
OIDC_ALLOW_SIGNUP=false # default false (least-privilege) — set to true to auto-provision accounts
OIDC_CLIENT_SECRETis a secret. Inject it via an environment variable, a secret manager (Vault, Doppler, AWS SSM, K8s Secret), or a.envfile that is never committed. FA never logs or returns it via any API.
IdP-specific notes
| IdP | Issuer URL pattern | Notes |
|---|---|---|
| Okta | https://your-org.okta.com or https://your-org.okta.com/oauth2/default | Use the Authorization Server issuer, not just the org domain. |
| Azure AD / Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 | Enable email claim via "Token configuration" in Azure Portal. |
| Auth0 | https://your-tenant.auth0.com | Enable OIDC-conformant mode; add email to the ID token claims. |
| Keycloak | https://your-keycloak/realms/<realm> | The realm's discovery URL is https://your-keycloak/realms/<realm>/.well-known/openid-configuration. |
| Google Workspace (generic OIDC) | https://accounts.google.com | Works in addition to the FA-native Google OAuth path. |
How it works
- FA performs OIDC Discovery at startup-per-login (with a 1-hour TTL cache):
GET <OIDC_ISSUER>/.well-known/openid-configuration→ fetchesauthorization_endpoint,token_endpoint, andjwks_uri. - The
id_tokenis verified locally (RSA-SHA256 signature against the IdP's JWKS, plusiss,aud,exp,nonce, andemail_verified). No secret is ever sent to a verification service. - Nonce is used for anti-replay (state cookie + signed
noncein the token). - OIDC is enabled iff
OIDC_ISSUER,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET, andOIDC_REDIRECT_URIare all set. - Sign-in is invite-only by default (
OIDC_ALLOW_SIGNUP=false): the IdP-verified email must match a pre-created FA user account. Unknown emails are rejected with a clear message directing the user to ask an admin. SetOIDC_ALLOW_SIGNUP=trueto let any IdP-authenticated user self-provision an FA account with the least-privilegeapproverrole.
User provisioning
By default (OIDC_ALLOW_SIGNUP=false), an admin must create each FA user account before they can sign in via SSO:
curl -s -X POST https://your-fa-domain.com/api/users \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alice@company.com", "name": "Alice", "role": "approver"}'Then Alice can sign in with SSO using alice@company.com. The IdP-verified email is the link key.
With OIDC_ALLOW_SIGNUP=true, first-time SSO users are auto-provisioned with role approver. Admins can promote them afterwards via PATCH /api/users/:id.
Identity linking
On a successful SSO login, FA records an oidc entry in the identities table (provider='oidc', provider_subject=<sub>). On subsequent logins with the same sub, the identity row is found directly without an email lookup — making email changes at the IdP safe after first login.
Users & Roles
Feature Agent has a built-in user system with two roles:
- Admin — Can manage projects, users, and all features via the dashboard or API
- Approver — Linked to specific projects; can approve features for those projects
Creating Your First Admin User
After starting the server, create an admin user via the API (using the legacy ADMIN_API_KEY or dev mode):
curl -s -X POST http://localhost:3100/api/users \
-H "Content-Type: application/json" \
-d '{"email": "admin@company.com", "name": "Admin", "role": "admin"}' | jq .Save the returned api_key — use it to log into the dashboard and for all admin API calls going forward.
Creating Approvers
# Create the approver
APPROVER=$(curl -s -X POST http://localhost:3100/api/users \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "po@company.com", "name": "PO Smith", "role": "approver"}')
APPROVER_ID=$(echo $APPROVER | jq -r .id)
# Link them to a project
curl -s -X POST http://localhost:3100/api/users/$APPROVER_ID/projects \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d "{\"project_id\": \"$PROJECT_ID\"}"The approver can now log into the dashboard with their API key and approve features for their linked projects.
Auth Model
| Key Type | Who Uses It | What It Can Do |
|---|---|---|
Project API key (fa_...) | External systems (CI, apps) | Submit features, list features, poke for updates |
Admin user key (fa_...) | Human admins | Everything — project CRUD, user CRUD, approve any feature |
Approver user key (fa_...) | Product owners / approvers | Approve features for linked projects, view scoped features |
Legacy ADMIN_API_KEY env var | Backwards compatibility | Same as admin user key |
You can manage all of this from the dashboard's Users tab (admin only), or via the API.
Protected Projects
Projects can be marked as protected to prevent accidental deletion:
# Set on creation
curl -X POST http://localhost:3100/api/projects \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "repo_url": "...", "autonomy_mode": "po_approval", "protected": true}'
# Or update an existing project
curl -X PATCH http://localhost:3100/api/projects/:id \
-H "Content-Type: application/json" \
-d '{"protected": true}'Protected projects return 403 on delete attempts. The dashboard shows a "protected" badge instead of a delete button.
Self-Managed Development
Feature Agent is enrolled as its own project. This means you can submit feature requests for Feature Agent itself, and the agent will implement them on the feature-agent-self branch with po_approval mode.
The featureagent project is protected and configured with:
- Autonomy mode:
po_approval— features require approval before implementation - Target branch:
feature-agent-self - Auto PR: enabled
- Notifications: Telegram
To submit a feature for Feature Agent:
curl -X POST http://localhost:3100/api/features \
-H "Authorization: Bearer $FEATUREAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Add dark mode to dashboard",
"description": "Add a light/dark mode toggle to the admin dashboard..."
}'The feature will appear as awaiting_approval in the dashboard. After approval, the agent clones the featureagent repo, creates a feature branch, implements the change with tests, and opens a draft PR.
Diagnostic Tool
Verify your environment is correctly configured for headless Claude Code operation:
npm run diagnose
npm run diagnose -- --prompt "Create a hello.txt file"The diagnostic spawns Claude Code the same way the agent does and reports success/failure with detailed error messages for common issues (auth, permissions, timeout).
Production Deployment
Environment Variables
# Server
PORT=3100
FA_SECRET_MODE=file # 'file' (default) | 'managed' — see "Secrets management" below
ADMIN_API_KEY=a-strong-random-key-here # Legacy admin key (optional — user-based admin auth is preferred)
# Storage
DATABASE_PATH=/var/featureagent/data/featureagent.db
WORKSPACE_DIR=/var/featureagent/workspaces
# Backup (VACUUM INTO snapshots — see docs/OPERATIONS.md §7 for the restore runbook)
BACKUP_DIR=./backups # Directory for snapshot files (created automatically)
BACKUP_RETENTION=14 # Keep N most-recent snapshots; older ones pruned per backup
BACKUP_INTERVAL_MS=0 # 0 = disabled (default). Set e.g. 3600000 for hourly backups
# Agent
AGENT_POLL_INTERVAL_MS=30000
AGENT_MAX_CONCURRENCY=3
AGENT_MODEL=sonnet
AGENT_MAX_TURNS=100
AGENT_MAX_RETRIES=3
AGENT_ALLOWED_TOOLS= # Comma-separated allowlist (empty = all tools)
AGENT_PERMISSIONS=dangerously-skip-permissions
CLAUDE_CLI_PATH=/usr/local/bin/claude
# Runtime (sandbox) — see docs/design/engine-runtime.md
RUNTIME=local # 'local' (host) or 'docker' (isolated container)
FA_RUNTIME_IMAGE=fa-runtime:latest # Image for RUNTIME=docker (needs git + node + claude)
# Curated images: fa-runtime (Node/TS), fa-runtime-python
# (Python), fa-runtime-browser (Chromium + Playwright).
# Build: docker build -t <tag> containers/<dir>
# Select per-project/feature via the runtime_image API
# field. See docs/USER_GUIDE.md §"Governed browser tooling"
# for the full governed-browse + verify_command + verify_gate recipe.
FA_RUNTIME_NETWORK=none # 'none' | 'bridge' | <docker network>
# CORS
CORS_ALLOWED_ORIGINS= # Comma-separated global origins (projects can set their own)
# Auth
ANTHROPIC_API_KEY=sk-ant-...
# Integrations
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_WHATSAPP_FROM=whatsapp:+1...
GITHUB_TOKEN=ghp_...
PR_CHECK_INTERVAL_MS=600000 # Per-feature throttle for PR merge/review polling (default 10 min)
# Webhooks
WEBHOOK_MAX_RETRIES=3
WEBHOOK_TIMEOUT_MS=10000
# Fleet Attention Queue SLA thresholds (spec 057)
# How long a human-gated feature can wait before it is flagged as a breach in the
# Fleet Overview attention queue (/fleet.html) and the GET /api/features/admin/attention endpoint.
FLEET_APPROVAL_SLA_HOURS=24 # awaiting_approval → operator must approve within N hours
FLEET_CLARIFICATION_SLA_HOURS=24 # clarification_needed → operator must answer within N hours
FLEET_MERGE_SLA_HOURS=48 # implemented+PR → operator must merge within N hours
# Fleet Attention Alerts — push SLA breaches to project notification channels (spec 057 phase 2)
# Opt-in; off by default. A project opts in by configuring notification channels (existing mechanism).
FLEET_ATTENTION_ALERTS_ENABLED=false # Set to 'true' to enable breach push-notifications
FLEET_ATTENTION_SWEEP_INTERVAL_MS=900000 # How often to check for new breaches (default 15 min)Running with systemd
# /etc/systemd/system/featureagent.service
[Unit]
Description=Feature Agent
After=network.target
[Service]
Type=simple
User=featureagent
WorkingDirectory=/opt/featureagent
ExecStart=/usr/bin/node dist/index.js
EnvironmentFile=/opt/featureagent/.env
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsudo systemctl enable featureagent
sudo systemctl start featureagentDeploy with Docker Compose (one command)
The fastest path from a fresh clone to a running, persistent FA with per-run agent sandboxing is Docker Compose. The compose file keeps RUNTIME=docker as the default so the safe path (isolated per-run containers) is also the easy path — you never need to opt out of safety to get started.
Prerequisites: Docker 20.10+ and Docker Compose v2 (docker compose version).
# 1. Copy the example env file and fill in your values
cp .env.example .env
# Required: ANTHROPIC_API_KEY, GITHUB_TOKEN, ADMIN_API_KEY
# Set RUNTIME=docker (already the compose default — do not set RUNTIME=local here)
# 2. Start FA in the background
docker compose up -d
# 3. Open the dashboard
open http://localhost:3100FA is now running with:
- Persistent state —
data/(SQLite DB) andworkspaces/stored in named Docker volumes; they survivedocker compose restartand image rebuilds. - Per-run agent sandboxing — FA launches each agent run inside a fresh container via
RUNTIME=docker; the untrusted agent never touches the host. - Auto-restart — the service restarts automatically on crash or host reboot.
Docker socket trust posture. The compose file mounts /var/run/docker.sock into the FA container so FA (the trusted Governor/orchestrator) can spawn per-run sandbox containers on the host daemon. This grants the FA process the ability to control Docker on the host, which is effectively host-root access. This is acceptable only because FA is the Governor, not the untrusted agent — keep RUNTIME=docker (the default) so the agents themselves remain sandboxed. Never set RUNTIME=local in a compose deployment; that removes the per-run isolation and defeats the purpose of the socket mount.
oauth auth mode (optional). If you prefer Claude OAuth credentials over an API key, uncomment the FA_AGENT_OAUTH_DIR volume line in docker-compose.yml and set FA_AGENT_AUTH=oauth in .env. FA will mount your local ~/.claude creds directory read-only into each agent container.
Useful operations:
docker compose logs -f fa # Tail live FA logs
docker compose restart fa # Restart without rebuilding
docker compose up -d --build # Rebuild image after a code change
docker compose down # Stop (volumes are preserved)
docker compose down -v # Stop AND delete state volumes (destructive!)See docs/OPERATIONS.md for the full operations runbook (state, backup, and the relationship to the systemd path).
Secrets management (FA_SECRET_MODE)
FA supports two modes for the admin API key:
| Mode | Behaviour |
|---|---|
file (default) | FA self-provisions ADMIN_API_KEY at startup if absent and writes it to .env. Back-compat; existing installs unaffected. |
managed | FA reads ADMIN_API_KEY from the environment only. If the key is absent at startup, FA fails closed with an actionable error and writes nothing to disk. Use this with a secret manager (Vault, K8s Secrets, Doppler) that injects env vars at launch. |
How to use managed mode:
- Set
FA_SECRET_MODE=managedin your deployment config (systemdEnvironmentFile, K8senvFrom, etc.). - Inject
ADMIN_API_KEY=<your-key>via your secret manager before FA starts. - FA reads the injected value on startup — no
.envwrite, no self-provisioning.
Admin key rotation (see OPERATIONS.md for the full runbook):
# Rotate via the API (admin auth required):
curl -X POST https://your-fa-host/api/admin/rotate-admin-key \
-H "Authorization: Bearer <current-admin-key>"
# → { "admin_key": "fa_<new-64-hex>" }In file mode, FA rewrites the .env file immediately and the new key is active in-process. In managed mode, FA updates in-process only — update your secret manager and redeploy to persist.
Security Checklist
- [ ] Create admin users and use their API keys instead of the legacy
ADMIN_API_KEY - [ ] Set
ADMIN_API_KEYas a fallback to protect project management endpoints - [ ] Use fine-grained GitHub tokens with minimal repo access
- [ ] Run in an isolated environment when using
dangerously-skip-permissions - [ ] Restrict network egress to required endpoints only
- [ ] Keep
WORKSPACE_DIRon a separate volume - [ ] Regularly rotate API keys and tokens
- [ ] Monitor agent activity via the dashboard and webhooks
Troubleshooting
Agent not processing features
- Check agent status:
curl http://localhost:3100/api/status - Ensure Claude Code is authenticated:
claude --print "hello" - Check logs for errors
- Verify features are in
queuedstatus (not stuck inanalyzingorclarification_needed)
Claude Code permission errors
If features fail with permission errors:
- Use
AGENT_PERMISSIONS=dangerously-skip-permissionsin isolated environments - Or configure Claude Code's
.claude/settings.jsonto auto-approve tools
Webhook delivery failures
- Check your endpoint is reachable from the Feature Agent server
- Verify HTTPS certificates are valid
- Increase
WEBHOOK_TIMEOUT_MSfor slow endpoints - Check logs for retry messages
WhatsApp messages not sending
- Verify Twilio credentials in
.env - Ensure recipients have joined the Twilio sandbox (for testing)
- Check phone numbers are in
whatsapp:+15551234567format - Verify
notifications_enabledis true on the project
Database issues
# Reset the database (dev only!)
npm run db:resetThe SQLite database auto-creates on first run. If corrupted, delete the .db file and restart.