CamelAGI Extensions
CamelAGI ships with five extension systems that add automation, safety, and customization on top of the core agent loop. Each extension is optional and can be configured independently through ~/.camelagi/config.yaml.
Table of Contents
Compaction
Source: src/runtime/compact.ts
Compaction prevents the conversation context from exceeding the model’s token window by automatically summarizing older messages and flushing durable facts to memory files.
How It Works
-
Token estimation – After each turn, the total token count is estimated using a 4-characters-per-token heuristic (
CHARS_PER_TOKEN = 4). -
Trigger threshold – Compaction fires when the estimated token count reaches 80% of the configured
maxTokens(COMPACTION_TRIGGER_RATIO = 0.8). -
History split – The conversation is divided into two parts:
- Old messages – everything before the last
keepTurnsuser turns. - Recent messages – the last
keepTurnsturns (default 6), preserved verbatim. - A “turn” boundary is defined by each
userrole message. - If there are fewer turns than
keepTurns, compaction is skipped (nothing old enough to summarize).
- Old messages – everything before the last
-
Memory flush – Before summarizing, the old messages are sent to the model with a dedicated prompt that extracts durable facts (decisions, preferences, file paths, dates). The extracted bullets are appended to a daily memory file at
~/.camelagi/workspace/memory/YYYY-MM-DD.mdunder a timestamped## HH:MM:SS (auto-flush)heading. If the combined text of old messages is shorter than 200 characters, the flush is skipped (nothing worth extracting). The input is capped at 30,000 characters (MEMORY_FLUSH_MAX_CHARS). If the model returns"NOTHING"or fewer than 10 characters, nothing is written. Memory flush is best-effort; failures are silently ignored. -
Summarize – The old messages are then summarized by the model. The summary replaces the old messages as a single
usermessage wrapped in[Previous conversation summary]/[End of summary — conversation continues below]markers. -
Validation – After summarization, the compacted result is compared against the original. If the compacted history’s estimated token count is not smaller than the original, compaction is skipped with a warning to stderr:
"⚠ Compaction skipped: result (X tokens) >= original (Y tokens)". This prevents pathological cases where the summary is longer than the input. -
Result – The compacted history is
[summary message, ...recent messages], significantly reducing token count while preserving context.
Configuration
compaction:
enabled: true # Enable/disable automatic compaction (default: true)
maxTokens: 100000 # Token budget; compaction triggers at 80% of this value
keepTurns: 6 # Number of recent user turns to preserve verbatim
Manual Trigger
- TUI: Type
/compactto trigger compaction immediately regardless of token count. - API: The gateway exposes compaction through the session management endpoints.
Cron Jobs
Source: src/extensions/cron.ts
Cron jobs let you schedule recurring or one-shot agent runs. They execute the agent with a configured prompt, save the conversation to a dedicated session, and support error backoff.
Two Sources of Jobs
| Source | Location | Mutability |
|---|---|---|
| Config-defined | config.yaml under cron: array |
Read-only at runtime |
| Runtime-defined | ~/.camelagi/cron/jobs.json |
Full CRUD via tools and CLI |
Runtime jobs are persisted to ~/.camelagi/cron/jobs.json (JSON format with version and jobs array). They survive server restarts.
Job Properties
| Property | Type | Description |
|---|---|---|
id |
string | Unique identifier |
name |
string | Human-readable name |
schedule |
string | Schedule expression (see formats below) |
prompt |
string | The message sent to the agent |
session |
string? | Session ID (defaults to cron-{id}) |
enabled |
boolean | Whether the job is active |
deleteAfterRun |
boolean? | Auto-remove after one-shot execution (default true for one-shots) |
Schedule Formats
Repeating interval:
5m # every 5 minutes
1h # every hour
1d # every day
30s # every 30 seconds
Supported units: s (seconds), m (minutes), h (hours), d (days).
Cron expression (5-field, limited):
*/5 * * * * # every 5 minutes (interval extracted from minute field)
*/15 * * * * # every 15 minutes
Important limitation: Only
*/Npatterns in the minute field are supported. Full cron scheduling semantics (day-of-week, hour, month, etc.) are not evaluated — the engine converts*/Nto a simple N-minute interval.Non-
*/Ncron expressions (e.g.,0 9 * * *) will silently fall back to a 1-minute interval, which is almost certainly not what you intend. Use duration syntax instead:
0 9 * * *(daily at 9am) → use1dinstead0 */2 * * *(every 2 hours) → use2hinsteadThe CLI
cron addcommand will warn when a non-*/Ncron expression is detected.
One-shot relative:
+20m # run once, 20 minutes from now
+2h # run once, 2 hours from now
When a runtime job uses a relative one-shot schedule (+20m), it is converted to an absolute ISO timestamp at creation time so it survives server restarts.
One-shot absolute (ISO 8601):
2026-03-14T09:00:00Z # run once at this exact time
Execution Behavior
- Repeating jobs run immediately on start, then schedule the next run after each completion.
- One-shot jobs wait for their delay/timestamp, run once, then auto-delete (unless
deleteAfterRunisfalse). - Each job runs the agent with up to 10 turns and a 120-second timeout.
- Conversation history is saved to the job’s session (
cron-{id}by default).
Error Handling and Backoff
On consecutive errors, the next run is delayed using an escalating backoff schedule:
| Consecutive Errors | Backoff Delay |
|---|---|
| 1 | 30 seconds |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5+ | 60 minutes |
The backoff resets to zero on a successful run.
Runtime Management
addRuntimeJob(job)– Creates and persists a new job. Auto-starts if the server is running.removeRuntimeJob(id)– Stops and deletes a runtime job.runJobNow(id)– Triggers any job (config or runtime) immediately, returning the agent’s response.getAllJobStatuses()– Returns status of all jobs (active and inactive), includinglastRunAt,lastStatus,lastError, andrunningstate.stopAllCronJobs()– Stops all active jobs (used during shutdown).
Storage
Runtime jobs are stored at:
~/.camelagi/cron/jobs.json
Format:
{
"version": 1,
"jobs": [
{
"id": "daily-summary",
"name": "Daily Summary",
"schedule": "1d",
"prompt": "Summarize today's notes",
"enabled": true,
"source": "runtime",
"createdAt": 1710000000000
}
]
}
Config Example
cron:
- id: morning-brief
name: Morning Brief
schedule: "1d"
prompt: "Give me a morning briefing"
enabled: true
- id: check-health
name: Health Check
schedule: "*/30 * * * *"
prompt: "Check system health"
enabled: true
Skills
Source: src/extensions/skills.ts
Skills are reusable instruction sets that get injected into the system prompt. They let you teach CamelAGI domain-specific behavior without modifying core code.
Directory Structure
~/.camelagi/skills/
my-skill/
SKILL.md # Required: skill definition
another-skill/
SKILL.md
Each skill lives in its own subdirectory under ~/.camelagi/skills/. The directory name is used as the skill name unless overridden in frontmatter. Only directories containing a SKILL.md file are recognized.
SKILL.md Format
A skill file consists of optional YAML frontmatter followed by the skill content (Markdown):
---
name: code-reviewer
description: Reviews code for best practices
---
# Code Review Skill
When asked to review code:
1. Check for security vulnerabilities
2. Verify error handling
3. Suggest performance improvements
...
Frontmatter fields (all optional):
| Field | Default | Description |
|---|---|---|
name |
Directory name | Display name of the skill |
description |
"" (empty) |
Short one-line description |
If the --- frontmatter block is missing, the entire file content is treated as the skill body.
System Prompt Injection
Skills are formatted into an ## Active Skills section in the system prompt:
## Active Skills
### code-reviewer -- Reviews code for best practices
[skill content here]
### another-skill
[skill content here]
Skills are added in filesystem order until the total character budget is exhausted.
Size Limits
The total combined size of all skill content injected into the system prompt is capped at 30,000 characters (MAX_SKILLS_TOTAL_CHARS). Skills are added sequentially; once the limit is reached, remaining skills are silently dropped. Keep individual skills concise to ensure all of them fit.
TUI
/skills– Lists all loaded skills by name.
Hooks
Source: src/extensions/hooks.ts
Hooks are shell scripts (.sh) or JavaScript files (.js) that run at specific lifecycle points during agent execution. They provide a way to add logging, notifications, auditing, or custom side effects without modifying the agent code.
Four Lifecycle Points
| Hook Point | When It Fires | Typical Uses |
|---|---|---|
before_prompt |
Before the user’s message is sent to the model | Logging, input sanitization |
after_response |
After the model returns a response | Notifications, analytics |
before_tool |
Before a tool call is executed | Auditing, access control |
after_tool |
After a tool call completes | Result logging, post-processing |
File Naming Convention
Hook scripts must follow the pattern:
{hook_point}.{name}.{sh|js}
Examples:
~/.camelagi/hooks/
before_prompt.log.sh
after_response.notify.sh
before_tool.audit.sh
after_tool.record.js
before_tool.security-check.sh
- The hook point must be one of the four valid points listed above.
- The name can contain dots (parts between the first dot and the file extension are joined).
- Files not ending in
.shor.jsare ignored. - Files with fewer than three dot-separated parts are ignored.
Environment Variables
Context is passed to hook scripts via environment variables prefixed with CAMELAGI_HOOK_:
| Variable | Always Set | Description |
|---|---|---|
CAMELAGI_HOOK_POINT |
Yes | The lifecycle point (before_prompt, etc.) |
CAMELAGI_HOOK_SESSION |
If available | Current session ID |
CAMELAGI_HOOK_MESSAGE |
before_prompt |
The user’s message |
CAMELAGI_HOOK_RESPONSE |
after_response |
The model’s response (truncated to 10,000 chars) |
CAMELAGI_HOOK_TOOL |
before_tool, after_tool |
Name of the tool being called |
CAMELAGI_HOOK_TOOL_ARGS |
before_tool, after_tool |
JSON-encoded tool arguments |
CAMELAGI_HOOK_TOOL_RESULT |
after_tool |
Tool execution result (truncated to 10,000 chars) |
The full process environment is also inherited, so hooks have access to PATH, HOME, and other standard variables.
Timeout Behavior
Each hook script has a 10-second timeout (HOOK_TIMEOUT_MS = 10_000). If a script exceeds this limit, it is killed and an error is logged to stderr. The agent continues normally – hook failures never block the agent loop.
Scripts are executed synchronously (execSync) with stdio: "pipe", meaning their stdout/stderr is captured but not displayed to the user. Errors are written to the process stderr stream.
Example Hook Scripts
Logging all tool calls (before_tool.log.sh):
#!/bin/bash
echo "$(date -Iseconds) TOOL=$CAMELAGI_HOOK_TOOL ARGS=$CAMELAGI_HOOK_TOOL_ARGS" >> ~/.camelagi/hooks/tool.log
Desktop notification on response (after_response.notify.sh):
#!/bin/bash
osascript -e "display notification \"CamelAGI responded\" with title \"CamelAGI\""
Audit write operations (before_tool.audit.sh):
#!/bin/bash
if [ "$CAMELAGI_HOOK_TOOL" = "Write" ] || [ "$CAMELAGI_HOOK_TOOL" = "Edit" ]; then
echo "$(date -Iseconds) WRITE session=$CAMELAGI_HOOK_SESSION tool=$CAMELAGI_HOOK_TOOL args=$CAMELAGI_HOOK_TOOL_ARGS" >> ~/.camelagi/audit.log
fi
Configuration
Hooks are enabled/disabled globally:
hooks:
enabled: true # Set to false to skip all hooks
The ~/.camelagi/hooks/ directory is created automatically when needed via ensureHooksDir().
Approvals
Source: src/extensions/approvals.ts, src/extensions/approval-forward.ts
The approval system gates dangerous tool calls behind user confirmation. It prevents the agent from executing write operations, shell commands, or other side-effecting actions without explicit consent.
Three Modes
| Mode | Behavior |
|---|---|
off |
All tool calls execute immediately. Zero friction. This is the default. |
smart |
Read-only tools are auto-approved; write/exec tools require approval. |
always |
Every tool call requires explicit approval. |
Auto-Approved Tools in Smart Mode
The following tools are considered read-only and are auto-approved when the mode is smart:
ReadGlobGrepWebSearchWebFetchmemory_searchmemory_get
All other tools (including Bash, Write, Edit, Agent, apply_patch) require approval in smart mode.
Allowlist Syntax and Patterns
The allowlist lets you pre-approve specific tool calls so they bypass the approval prompt even in smart or always mode. Entries are stored in the config under approvals.allowlist.
Bare tool name – matches all calls to that tool:
Read
Glob
Tool with pattern – matches calls where the relevant argument matches the glob:
Bash:npm * # Allow any npm command
Bash:git status * # Allow git status
Write:/tmp/* # Allow writing to /tmp
Edit:src/*.ts # Allow editing TypeScript files in src/
apply_patch:* # Allow all patches
Pattern matching rules:
- For
Bashtools, the pattern is matched against thecommandargument. - For
WriteandEdittools, the pattern is matched against thefile_pathargument. - For
apply_patch, only the wildcard*pattern is supported (blanket allow). - Glob matching uses
*as a wildcard that matches any characters.
Auto-generated allowlist entries:
When a user selects “Allow Always” for a tool call, an entry is automatically added to the allowlist:
Bashcommands: the first word of the command is extracted (e.g.,npm install foobecomesBash:npm *).Write/Editcalls: the exact file path is added (e.g.,Edit:src/main.ts).- Other tools: the bare tool name is added.
Approval Flow
- Before each tool call,
checkApproval()is called. - If the mode is
off, or the tool/args match the allowlist, or the tool is read-only insmartmode: the call proceeds immediately (returnsnull). - Otherwise, an
ApprovalRequestis created with a unique ID, tool name, arguments, and a human-readable preview. - The request is emitted as an event, and
waitForDecision()blocks until the user responds or the timeout expires. - The user submits one of three decisions:
- Allow Once – execute this call only.
- Allow Always – execute this call and add it to the allowlist for future calls.
- Deny – reject the tool call.
Preview Format
The approval prompt shows a preview of what the tool will do:
| Tool | Preview Format |
|---|---|
Bash |
First 200 characters of the command |
Write |
write -> /path/to/file |
Edit |
edit -> /path/to/file |
Agent |
spawn agent: [first 100 chars of prompt] |
apply_patch |
patch (N lines) |
| Other | toolName(JSON args truncated to 120 chars) |
Approval Channels
TUI (interactive terminal):
When running in the TUI, approval prompts appear inline. The user can approve or deny directly from the terminal.
Telegram (inline buttons):
When the agent is running headless (via HTTP API, cron job, or boot), approval requests can be forwarded to a Telegram chat. The approval-forward.ts module sends a message with three inline keyboard buttons:
- “Allow” (
allow-once) - “Always” (
allow-always) - “Deny” (
deny)
The Telegram bot used for forwarding is the same bot configured for the Telegram channel. It is registered at startup via registerForwardBot().
Headless forwarding config:
approvals:
forwardTo: 123456789 # Your Telegram user/chat ID
If no bot is registered or forwardTo is not set, the forwarding silently fails and the timeout/fallback behavior takes over.
Timeout and Fallback
When waiting for a decision, a timeout is applied. If the user does not respond within the timeout period:
- The fallback behavior is configurable as either
"deny"(reject the call) or"allow"(permit it). "deny"is the safe default for most deployments."allow"may be useful for trusted automation pipelines where you want the agent to proceed even if the approval channel is unavailable.
Config Examples
Disable approvals (default):
approvals:
mode: "off"
Smart mode with allowlist:
approvals:
mode: "smart"
allowlist:
- "Bash:git *"
- "Bash:npm *"
- "Edit:src/*"
- "Read"
Always mode with Telegram forwarding:
approvals:
mode: "always"
forwardTo: 123456789
allowlist:
- "Bash:ls *"
- "Read"
- "Glob"
- "Grep"
Smart mode for a project that only writes to specific directories:
approvals:
mode: "smart"
allowlist:
- "Write:/Users/me/projects/myapp/src/*"
- "Edit:/Users/me/projects/myapp/src/*"
- "Bash:npm *"
- "Bash:node *"