Skip to content

Configuration

Upgrading from pre-1.3.14? Read Upgrading from pre-1.3.14 before restarting the bot — the security hardening changed a few defaults and your admin/Telegram access may stop working until you complete the migration.

Runtime config file: ~/.sygen/config/config.json.

Seed source: <repo>/config.example.json (source checkout) or packaged fallback sygen_bot/_config_example.json (installed mode).

Config Creation

Primary path: sygen onboarding (interactive wizard) writes config.json with user-provided values merged into AgentConfig defaults.

Load & Merge Behavior

Config is merged in two places:

  1. sygen_bot/__main__.py::load_config()
  2. creates config on first start (copy from config.example.json or Pydantic defaults),
  3. deep-merges runtime file with AgentConfig defaults,
  4. writes back only when new keys were added.
  5. sygen_bot/workspace/init.py::_smart_merge_config()
  6. shallow merge {**defaults, **existing} with config.example.json,
  7. preserves existing user top-level keys,
  8. fills missing top-level keys from config.example.json.

Normalization detail:

  • onboarding and runtime config load normalize gemini_api_key default to string "null" in persisted JSON for backward compatibility.
  • AgentConfig validator converts null-like text ("", "null", "none") to None at runtime.

Runtime edits persisted through config helpers include /model changes (model/provider/reasoning), webhook token auto-generation, and API token auto-generation.

API config persistence note:

  • load_config() intentionally does not auto-add the api block during default deep-merge (beta gating).
  • sygen api enable writes the api block (including generated token) into config.json.

External API Secrets (~/.sygen/.env)

User-defined environment secrets for external APIs (e.g. PPLX_API_KEY, DEEPSEEK_API_KEY).

Standard dotenv syntax:

PPLX_API_KEY=sk-xxx
DEEPSEEK_API_KEY=sk-yyy
export MY_VAR="quoted value"

Propagation:

  • host CLI execution: merged into subprocess env via _build_subprocess_env()
  • Docker exec: injected as -e flags via docker_wrap()
  • Docker container creation: injected as -e flags via _start_container()
  • sub-agents and background tasks: inherited through the same execution paths

Priority (highest to lowest):

  1. existing host environment variables (never overridden)
  2. provider-specific config (e.g. gemini_api_key in config.json)
  3. .env values (fill gaps only)

Changes take effect on the next CLI invocation (mtime-based cache invalidation, no restart needed).

AgentConfig (sygen_bot/config.py)

Field Type Default Notes
log_level str "INFO" Applied at startup unless CLI --verbose is used
provider str "claude" Default provider
model str "opus" Default model ID
sygen_home str "~/.sygen" Runtime home root
idle_timeout_minutes int 1440 Session freshness idle timeout (0 disables idle expiry)
session_age_warning_hours int 12 Adds /new reminder after threshold (every 10 messages)
daily_reset_hour int 4 Daily reset boundary hour in user_timezone
daily_reset_enabled bool false Enables daily session reset checks
user_timezone str "" IANA timezone used by cron/heartbeat/cleanup/session reset
max_budget_usd float \| None None Passed to Claude CLI
max_turns int \| None None Passed to Claude CLI
max_session_messages int \| None None Session rollover limit
permission_mode str "bypassPermissions" Default CLI --permission-mode. Used as fallback when a role has no entry in permission_mode_by_role. Valid values: acceptEdits, auto, bypassPermissions, default, dontAsk, plan.
permission_mode_by_role dict[str, str] {"admin": "bypassPermissions", "operator": "acceptEdits", "viewer": "default"} Per-role CLI permission mode. See Per-role permission modes below.
cli_timeout float 1800.0 Legacy/global timeout. Still used by cron/webhook cron_task, inter-agent turns, stale-process heartbeat cleanup, and as fallback for unknown timeout paths
reasoning_effort str "medium" Default Codex reasoning level
file_access str "all" File access scope (all, home, workspace) for file sends and API GET /files; unknown values fall back to workspace-only
gemini_api_key str \| None None Config fallback key injected for Gemini API-key mode
transport str "telegram" Messaging transport: "telegram" or "matrix"
transports list[str] [] List of transports to run in parallel (e.g. ["telegram", "matrix"]). When empty, falls back to single transport value.
telegram_token str "" Telegram bot token (required when transport=telegram)
allowed_user_ids list[int] [] Telegram user allowlist (applies in both private and group chats)
allowed_group_ids list[int] [] Telegram group allowlist (which groups the bot can operate in; default [] = no groups, fail-closed). In groups, both the group and the user must be allowlisted
group_mention_only bool false Mention/reply gating in group rooms. Telegram: filter only (no auth bypass). Matrix: in non-DM rooms this bypasses allowed_users and uses room + mention/reply as gate
matrix MatrixConfig see below Matrix homeserver connection (required when transport=matrix)
streaming StreamingConfig see below Streaming tuning
docker DockerConfig see below Docker sidecar config
heartbeat HeartbeatConfig see below Background heartbeat config
cleanup CleanupConfig see below Daily file-retention cleanup
memory MemoryConfig see below Memory system settings (injection, maintenance)
webhooks WebhookConfig see below Webhook HTTP server config
fileshare FileshareConfig see below Built-in fileshare HTTP server config
api ApiConfig see below Direct WebSocket API server config
cli_parameters CLIParametersConfig see below Provider-specific extra CLI flags
image ImageConfig see below Incoming image processing settings
timeouts TimeoutConfig see below Path-specific timeout policy (normal, background, subagent)
tasks TasksConfig see below Delegated background task system (TaskHub)
scene SceneConfig see below Scene indicators and technical footer
transcription TranscriptionConfig see below Audio/video transcription settings (language, model, custom command)
update_check bool true Enables periodic update observer (UpdateObserver). Verifies actual installed version via subprocess before notifying — will not offer upgrades for versions already installed externally.
topic_defaults dict[str, dict[str, str]] {} Per-topic default models. Keys are topic IDs (as strings), values are {"model": "<model_id>"}. Set via /topicmodel command. Resolution priority: @directive > topic_default > config.model
additional_directories list[str] [] Extra directories passed as --add-dir to Claude CLI, extending the sandbox beyond the workspace. Only absolute paths to existing directories are accepted.
interagent_port int 8799 Port for internal localhost API (InternalAgentAPI)
interagent InteragentConfig see below Inter-agent communication timeouts

Multi-transport behavior

When transports is empty (default), the single transport value is used. When transports contains multiple entries (e.g. ["telegram", "matrix"]), MultiBotAdapter starts all listed transports in parallel and transport is auto-set to the first entry. A model validator normalizes both fields at load time so they stay consistent.

Per-role permission modes

The permission_mode_by_role field maps user roles to CLI --permission-mode values. When a CLI session is started on behalf of a user, the resolver looks the user's role up in this map and passes the corresponding mode to the Claude CLI. System-initiated calls (cron, webhooks, background observers) run with the "admin" role.

Resolution order:

  1. If the caller provides a role and the role is present in permission_mode_by_role, use the mapped value.
  2. Otherwise, fall back to the top-level permission_mode field (legacy behaviour).

Recommended defaults:

Role Mode Effect
admin bypassPermissions No prompts — full access (unchanged legacy behaviour).
operator acceptEdits File edits auto-allowed; tool calls that can modify shared state (e.g. arbitrary shell commands) still ask for confirmation.
viewer default Prompts for every write and tool invocation.

Valid values are the same as the Claude CLI's --permission-mode flag: acceptEdits, auto, bypassPermissions, default, dontAsk, plan. Unknown values fail config validation at load time.

To opt out of per-role resolution entirely (legacy behaviour), set permission_mode_by_role to {} — the resolver then always returns the top-level permission_mode.

Role sources (who resolves the role)

The role threaded into permission_mode_by_role can come from four places:

Entry point Source Fallback when missing
REST / Admin WS role claim in the JWT access token global permission_mode
Telegram message.from_user.id mapped via telegram_user_ids in users.json global permission_mode
Inter-agent (cron, webhooks, ask_agent*) user_role="admin" or the SYGEN_CALLER_USER_ID env var that the framework injects from the authenticated transport global permission_mode
None of the above None global permission_mode

Per-user access control (users.json)

~/.sygen/_secrets/users.json stores user accounts used by the admin panel, the Telegram role resolver, and the inter-agent ACL. Each user record supports the following fields relevant to authorization:

Field Type Notes
username str Primary key; used as caller_user_id for inter-agent calls.
role str One of admin, operator, viewer. Maps to permission_mode_by_role.
active bool Inactive users are ignored by all role resolvers.
allowed_agents list[str] Agents this user may reach via the inter-agent bus. Empty list or ["*"] = no restriction. Otherwise the recipient must appear in the list. Enforced in addition to the per-agent can_call allow list.
telegram_user_ids list[int] Optional. Telegram from_user.id values that identify this user. When a Telegram message arrives from one of these ids, the user's role is used to pick the CLI permission mode.

Unknown users (no matching record) fall through to legacy behaviour so rolling deployments do not break in-flight sessions.

Example:

{
  "users": [
    {
      "username": "alice",
      "role": "operator",
      "active": true,
      "allowed_agents": ["main", "research"],
      "telegram_user_ids": [1234567]
    }
  ]
}

MatrixConfig

Field Type Default Notes
homeserver str "" Matrix homeserver URL (e.g. https://matrix.org)
user_id str "" Bot user ID (e.g. @sygen:matrix.org)
password str "" Password for initial login
access_token str "" Optional manual restore source; runtime normally persists credentials in the Matrix store
device_id str "" Optional manual restore source paired with access_token
allowed_rooms list[str] [] Room IDs or aliases the bot may operate in
allowed_users list[str] [] Matrix user IDs allowed to interact
store_path str "matrix_store" E2EE key store directory, relative to sygen_home

Notes:

  • first successful login persists credentials to ~/.sygen/<store_path>/credentials.json (mode 0o600), not back into config.json
  • when access_token and device_id are explicitly present in config.json, runtime restores from them and also mirrors them into the credentials store
  • The bot supports end-to-end encrypted rooms via matrix-nio[e2e].
  • allowed_rooms and allowed_users together form the Matrix auth model.

CLIParametersConfig

Field Type Default Notes
claude list[str] [] Extra args appended to Claude CLI command
codex list[str] [] Extra args appended to Codex CLI command
gemini list[str] [] Extra args appended to Gemini CLI command

Used by CLIServiceConfig for main-chat calls.

Argument shape note:

  • each list element is passed as one CLI argument; do not combine multiple shell flags into one string such as "--verbose --chrome"

Automation note:

  • cron/webhook cron_task runs use task-level cli_parameters from cron_jobs.json / webhooks.json (no merge with global cli_parameters).

TimeoutConfig

Field Type Default Notes
normal float 600.0 Default timeout for foreground chat turns (normal / normal_streaming)
background float 1800.0 Timeout for named background sessions (BackgroundObserver)
subagent float 3600.0 Reserved timeout bucket for sub-agent-specific paths
warning_intervals list[float] [60.0, 10.0] Warning thresholds for TimeoutController
extend_on_activity bool true Enables deadline extension when subprocess output is active
activity_extension float 120.0 Seconds added per granted extension
max_extensions int 3 Maximum activity-based extensions

Runtime sync behavior:

  • AgentConfig keeps backward compatibility with cli_timeout.
  • If cli_timeout != 600.0 and timeouts.normal is still default, runtime validation copies cli_timeout into timeouts.normal.
  • If timeouts.normal is explicitly set, it wins over cli_timeout.

Current execution-path usage:

  • foreground chat turns: resolve_timeout(config, "normal") -> timeouts.normal
  • named background sessions (/session): timeouts.background
  • delegated background tasks (TaskHub): tasks.timeout_seconds
  • cron + webhook cron_task: still config.cli_timeout
  • inter-agent turns: still config.cli_timeout
  • stale-process cleanup threshold: config.cli_timeout * 2

Implementation status note:

  • cli/timeout_controller.py and warning/extension config are implemented and tested.
  • provider wrappers and executor support TimeoutController in production paths.
  • normal/streaming/named-session/heartbeat flows create controllers via flows._make_timeout_controller(...).
  • timeout warning/extension callbacks are not yet wired to Telegram/API system-status output, so user-visible timeout status labels are not emitted by default.

TasksConfig

Field Type Default Notes
enabled bool true Enables shared delegated task system (TaskHub)
max_parallel int 5 Max concurrent running tasks per chat in TaskHub
timeout_seconds float 3600.0 Timeout per delegated task run

Task-Level Automation Overrides

Stored outside config.json in:

  • ~/.sygen/cron_jobs.json (CronJob)
  • ~/.sygen/webhooks.json (WebhookEntry, cron_task mode)

Common per-task fields:

  • execution: provider, model, reasoning_effort, cli_parameters
  • scheduling guards: quiet_start, quiet_end, dependency

Cron-only field:

  • timezone (per-job timezone override)

Behavior notes:

  • missing execution fields fall back to global config via resolve_cli_config(),
  • dependency is global across cron + webhook cron_task runs (shared DependencyQueue),
  • quiet-hour checks run only when per-task quiet fields are set (no fallback to global heartbeat quiet settings).

StreamingConfig

Field Type Default
enabled bool true
min_chars int 200
max_chars int 4000
idle_ms int 800
edit_interval_seconds float 2.0
max_edit_failures int 3
append_mode bool false
sentence_break bool true

DockerConfig

Field Type Default Notes
enabled bool false Master toggle
image_name str "sygen-sandbox" Docker image name
container_name str "sygen-sandbox" Docker container name
auto_build bool true Build image automatically when missing
mount_host_cache bool false Mount host ~/.cache into container (see below)
mounts list[str] [] Extra host directories mounted into sandbox (/mnt/...)
extras list[str] [] Optional AI/ML package IDs to install in the Docker image (see below)

Orchestrator.create() calls DockerManager.setup() when enabled. If setup fails, sygen logs warning and falls back to host execution.

mount_host_cache

Mounts the host's platform-specific cache directory into the container at /home/node/.cache:

Platform Host path
Linux ~/.cache (or $XDG_CACHE_HOME)
macOS ~/Library/Caches
Windows %LOCALAPPDATA%

Use case: browser-based skills (e.g. google-ai-mode) that use patchright/playwright need access to persistent browser profiles and browser binaries stored in the host cache. Without this, each container start requires a fresh CAPTCHA solve and Chrome download.

Disabled by default because it exposes the host cache directory to the sandbox.

mounts

User-defined directory mounts for project/data access inside Docker sandbox.

  • each entry is expanded (~, env vars), resolved, and validated as an existing directory
  • each entry is just a host directory path (for example "/home/you/projects"), not Docker host:container[:mode] syntax
  • invalid or missing entries are skipped with warnings
  • container target path is derived from host basename: /mnt/<sanitized-name>
  • duplicate target names are disambiguated as /mnt/name_2, /mnt/name_3, ...

Runtime note:

  • updates are typically managed via sygen docker mount|unmount
  • changing mounts requires bot restart (or sygen docker rebuild) to affect container run flags

extras

Optional AI/ML packages installed into the Docker sandbox image at build time. Each entry is an ID from the extras registry (sygen_bot/infra/docker_extras.py).

Available extras:

ID Name Category Size
ffmpeg FFmpeg Audio / Speech ~100 MB
whisper Faster Whisper Audio / Speech ~500 MB
opencv OpenCV Vision / OCR ~100 MB
tesseract Tesseract OCR Vision / OCR ~40 MB
easyocr EasyOCR Vision / OCR ~2.5 GB
pymupdf PyMuPDF Document Processing ~50 MB
pandoc Pandoc Document Processing ~80 MB
scipy SciPy Scientific / Data ~130 MB
pandas pandas Scientific / Data ~60 MB
matplotlib Matplotlib Scientific / Data ~60 MB
pytorch-cpu PyTorch (CPU) ML Frameworks ~800 MB
transformers HF Transformers ML Frameworks ~2 GB
playwright Playwright Web / Browser ~450 MB

Dependency resolution:

  • whisper depends on ffmpeg
  • easyocr and transformers depend on pytorch-cpu
  • dependencies are auto-resolved at build time

Managed via sygen docker extras-add|extras-remove or during onboarding wizard. Changes require sygen docker rebuild to take effect.

When extras are configured, the supervisor startup timeout is dynamically extended to accommodate longer Docker build times.

HeartbeatConfig

Field Type Default Notes
enabled bool false Master toggle
interval_minutes int 30 Loop interval
cooldown_minutes int 5 Skip if user active recently
quiet_start int 21 Quiet start hour in user_timezone
quiet_end int 8 Quiet end hour in user_timezone
prompt str default prompt Multiline default prompt references MAINMEMORY.md and cron_tasks/
ack_token str "HEARTBEAT_OK" Suppression token
group_targets list[HeartbeatTarget] [] Per-group/topic heartbeat targets with optional overrides

HeartbeatTarget

Each entry in group_targets identifies a specific group chat (and optional topic) to send heartbeat checks to. All optional fields override the global HeartbeatConfig when set; unset fields fall back to global values.

Field Type Required Default Notes
chat_id int yes Target group chat ID
topic_id int \| None no None Optional forum topic ID within the group
prompt str \| None no None Per-target prompt override (falls back to global prompt)
ack_token str \| None no None Per-target suppression token (falls back to global ack_token)
interval_minutes int \| None no None Per-target interval override (falls back to global interval_minutes)
quiet_start int \| None no None Per-target quiet-hour start (falls back to global quiet_start)
quiet_end int \| None no None Per-target quiet-hour end (falls back to global quiet_end)

ImageConfig

Field Type Default Notes
max_dimension int 3000 Maximum width/height in pixels; images exceeding this are resized proportionally
output_format str "webp" Target image format (e.g. webp, jpeg, png)
quality int 85 Compression quality for lossy formats (WebP, JPEG)

Applied to incoming images across all transports (Telegram, Matrix, API). See files/image_processor.py for implementation details.

TranscriptionConfig

Field Type Default Notes
language str "auto" Language code ("en", "ru", "de", etc.). "auto" = auto-detect
model str "small" Whisper model size: "tiny", "base", "small", "medium", "large"
command str \| null null Custom external command (see below)

Controls audio/video transcription behavior for voice messages and video notes.

Strategy chain

Transcription tries backends in this order:

  1. Custom command (only when command is set) — receives audio file path as argument, prints transcript to stdout
  2. OpenAI Whisper API — requires OPENAI_API_KEY in environment or .env
  3. Local whisper CLI — Python openai-whisper package
  4. whisper-cli — whisper.cpp (C++ implementation, 5-10x faster than Python on CPU)

First successful backend wins. If all fail, an error with installation hints is returned.

Custom command interface

When command is set, it is called as:

/path/to/your/command /path/to/audio.ogg

Environment variables passed to the command:

  • TRANSCRIPTION_LANGUAGE — configured language (e.g. "auto", "en")
  • TRANSCRIPTION_MODEL — configured model size (e.g. "small")

The command must print the transcript text to stdout and exit with code 0. On non-zero exit or empty output, the next strategy in the chain is tried.

Model sizes

Model Size Quality Speed (CPU)
tiny ~75 MB Basic Very fast
base ~150 MB Good Fast
small ~500 MB Great Medium
medium ~1.5 GB Excellent Slow
large ~3 GB Best Very slow

Quick setup

Fastest path for new users — cloud API (no local install):

# ~/.sygen/.env
OPENAI_API_KEY=sk-xxx

Fastest local path — whisper.cpp:

# Install whisper.cpp, then download model:
# https://github.com/ggerganov/whisper.cpp
whisper-cli --help  # verify installation

Hot-reloadable: yes (changes apply without restart).

SceneConfig

Field Type Default Notes
reaction_style str "seen" Emoji reactions: "off" = none, "seen" = 👀 on receipt + 👌 on done, "detailed" = 👀→🤔→✍️→💯→👌 real-time status
technical_footer bool false Appends model/token/cost/time footer to agent responses

CleanupConfig

Field Type Default Notes
enabled bool true Master toggle
media_files_days int 30 Retention for media files (telegram + matrix)
output_to_user_days int 30 Retention in workspace/output_to_user/
api_files_days int 30 Retention in workspace/api_files/
check_hour int 3 Local hour in user_timezone for cleanup run

Cleanup implementation detail:

  • cleanup is recursive (_delete_old_files walks nested files via rglob("*")),
  • after file deletion, empty subdirectories are pruned,
  • dated upload folders (.../YYYY-MM-DD/...) are cleaned when contained files exceed retention and directories become empty.

MemoryConfig

Field Type Default Notes
enabled bool true Master toggle for mechanical memory maintenance
module_line_limit int 80 Max lines per memory module (agent gets size warnings above this)
session_max_age_days int 30 Session cleanup threshold
check_hour int 4 Hour for memory maintenance (4 AM)
hook_compact_lines int 20 Max lines per module injected in periodic memory hook
inject_all_modules bool false Inject ALL modules (not just Always Load) into agent context
vector_search bool false Enable semantic vector search over memory facts
vector_model str "" Embedding model name; empty = auto-detect best available
vector_results int 5 Max facts returned per vector search query

Memory injection behavior

Memory modules are injected into the agent's context at two points:

  1. Session start — full content of selected modules is injected via --append-system-prompt
  2. Every 6 messagesMAINMEMORY_REMINDER hook re-injects module content (truncated to hook_compact_lines per module) to prevent knowledge loss after context compaction

Which modules are injected depends on inject_all_modules:

  • false (default) — only modules listed in the "Always Load" table in MAINMEMORY.md (typically user.md and decisions.md)
  • true — all .md files in memory_system/modules/ are injected

Token cost estimates

With 2 Always Load modules (~25 lines each): - Session start: ~1.5K tokens (one-time) - Hook (every 6 msgs): ~600 tokens with hook_compact_lines=20

With 5 modules and inject_all_modules=true: - Session start: ~3K tokens (one-time) - Hook (every 6 msgs): ~1.2K tokens with hook_compact_lines=20

For users with high-tier API plans who prioritize agent knowledge over token cost, set inject_all_modules=true and increase hook_compact_lines (e.g. 50 or 100).

Vector search (semantic memory)

Optional feature for semantic search over memory facts. Instead of injecting entire modules, the hook searches for facts relevant to the current conversation.

Installation:

pip install sygen[vector]    # ChromaDB + sentence-transformers (multilingual, ~500MB)

Enable:

"memory": {
  "vector_search": true
}

How it works:

  1. Memory module facts (list items) are automatically indexed as embeddings
  2. On each hook trigger, the user's message is used as a search query
  3. Top-N semantically relevant facts are injected instead of full modules
  4. Falls back to module-based injection if vector search returns no results

Model priority:

  1. sentence-transformers multilingual model (if installed) — best for non-English
  2. ChromaDB built-in ONNX model — English-focused, no extra dependencies
  3. Custom model via vector_model config option

Vector search complements (not replaces) the module-based system. Both can be active simultaneously — vector search provides targeted facts while Always Load ensures core context.

WebhookConfig

Field Type Default Notes
enabled bool false Master toggle
host str "127.0.0.1" Bind address (localhost by default)
port int 8742 HTTP server port
token str "" Global bearer fallback token (auto-generated when webhooks start)
max_body_bytes int 262144 Max request body size
rate_limit_per_minute int 30 Sliding-window rate limit

FileshareConfig

Field Type Default Notes
enabled bool false Master toggle
host str "127.0.0.1" Bind address
port int 8090 HTTP server port

When enabled, sets FILESHARE_URL and FILESHARE_DOWNLOADS environment variables for agent tools (used by send_large_file.py). See fileshare.md for full details.

ApiConfig

Field Type Default Notes
enabled bool false Master toggle
host str "0.0.0.0" Bind address
port int 8741 API HTTP/WebSocket port
token str "" Bearer/WebSocket auth token (generated by sygen api enable, with runtime generation fallback on API start)
chat_id int 0 Default API session chat (0 means fallback to first allowed_user_ids entry, else 1)
allow_public bool false Suppresses Tailscale-not-detected warning

Runtime note (Orchestrator._start_api_server + ApiServer._authenticate):

  • config.api.chat_id is used via truthiness (0 falls back),
  • fallback default comes from first allowed_user_ids entry (fallback 1),
  • per-connection auth payload may override via:
  • {"type":"auth","chat_id":...} (positive int),
  • optional channel_id (positive int) for per-channel session isolation (SessionKey.topic_id),
  • clients can override only for that connection; persisted default stays in config.

InteragentConfig

Field Type Default Notes
sync_timeout float 600.0 Timeout (seconds) for synchronous inter-agent requests via bus.send()
async_timeout float 3600.0 Timeout (seconds) for asynchronous inter-agent tasks via bus.send_async()

Configurable via config.json:

"interagent": {
    "sync_timeout": 600.0,
    "async_timeout": 3600.0
}

Runtime hot-reload (config_reload.py)

Orchestrator.create() starts ConfigReloader, which polls config.json every 5 seconds, validates it with AgentConfig, diffs top-level fields, and applies safe changes without restart.

Hot-reloadable top-level fields:

  • model, provider, reasoning_effort
  • cli_timeout, max_budget_usd, max_turns, max_session_messages
  • idle_timeout_minutes, session_age_warning_hours, daily_reset_hour, daily_reset_enabled
  • permission_mode, permission_mode_by_role, file_access, user_timezone
  • streaming, heartbeat, cleanup, cli_parameters
  • allowed_user_ids, allowed_group_ids, group_mention_only
  • transcription
  • timeouts is currently restart-required (not in hot-reloadable set)

Observer lifecycle caveat:

  • heartbeat/cleanup values hot-reload into config
  • observer start/stop is not hot-toggled
  • enabling heartbeat/cleanup after startup requires restart if the observer was not started initially

Restart-required top-level fields:

  • transport, telegram_token, matrix
  • docker, api, webhooks
  • sygen_home, log_level, gemini_api_key, timeouts, tasks

Restart classification is computed from AgentConfig top-level schema fields.

Model Resolution

ModelRegistry (sygen_bot/config.py):

  • Claude models are hardcoded: haiku, sonnet, opus.
  • Gemini aliases are hardcoded: auto, pro, flash, flash-lite.
  • Runtime Gemini models are discovered from local Gemini CLI files at startup.
  • Provider resolution (provider_for(model_id)):
  • Claude when in CLAUDE_MODELS,
  • Gemini when in aliases/discovered set or when model looks like gemini-*/auto-gemini-*,
  • otherwise Codex.

Timezone Resolution

resolve_user_timezone(configured) in sygen_bot/config.py:

  1. valid configured IANA timezone,
  2. $TZ env var,
  3. host system detection:
  4. Windows: local datetime tzinfo,
  5. POSIX: /etc/localtime symlink,
  6. fallback UTC.

Returns ZoneInfo when available, otherwise a UTC tzinfo fallback object with key="UTC" on systems without timezone data. Used by cron scheduling, session daily-reset checks, heartbeat quiet hours, and cleanup scheduling.

reasoning_effort

UI values: low, medium, high, xhigh.

Main-chat flow:

AgentConfig -> CLIServiceConfig -> CLIConfig -> CodexCLI (-c model_reasoning_effort=<value> when relevant).

Automation flow:

  • resolve_cli_config() applies reasoning effort only for Codex models that support the requested effort.

Codex Model Cache

Path: ~/.sygen/config/codex_models.json.

Behavior:

  • loaded at orchestrator startup (CodexCacheObserver.start()),
  • startup load is forced refresh (force_refresh=True),
  • checked hourly in background,
  • load_or_refresh() uses cache if <24h old, otherwise re-discovers via Codex app server,
  • consumed by /model wizard, resolve_cli_config() for cron/webhook validation, and /diagnose output.

Gemini Model Cache

Path: ~/.sygen/config/gemini_models.json.

Behavior:

  • loaded at orchestrator startup (GeminiCacheObserver.start()),
  • startup load uses cached data when fresh and refreshes only when stale/missing,
  • refreshed hourly in background,
  • refresh callback updates runtime Gemini model registry (set_gemini_models(...)) used by directives and model selector.

agents.json (Multi-Agent Registry)

Path: ~/.sygen/agents.json.

Top-level JSON array of SubAgentConfig objects. Each entry defines a sub-agent that runs alongside the main agent.

Managed via:

  • sygen agents add <name> (interactive CLI, currently Telegram-focused)
  • sygen agents remove <name> (CLI)
  • create_agent.py / remove_agent.py tool scripts (from within a CLI session)
  • manual file editing (auto-detected by FileWatcher)

SubAgentConfig fields

Field Type Required Default Notes
name str yes Unique lowercase identifier
transport str no "telegram" "telegram" or "matrix"
telegram_token str conditional Required when transport=telegram
matrix MatrixConfig conditional Required when transport=matrix
allowed_user_ids list[int] no [] Telegram user allowlist
allowed_group_ids list[int] no [] Telegram group allowlist
group_mention_only bool no inherited Mention/reply gating toggle (transport-specific behavior)
provider str no inherited Default provider
model str no inherited Default model
log_level str no inherited
idle_timeout_minutes int no inherited
session_age_warning_hours int no inherited
daily_reset_hour int no inherited
daily_reset_enabled bool no inherited
max_budget_usd float no inherited
max_turns int no inherited
max_session_messages int no inherited
permission_mode str no inherited
cli_timeout float no inherited
reasoning_effort str no inherited
file_access str no inherited
streaming StreamingConfig no inherited
docker DockerConfig no inherited
heartbeat HeartbeatConfig no inherited
cleanup CleanupConfig no inherited
webhooks WebhookConfig no inherited
api ApiConfig no disabled Disabled by default for sub-agents
cli_parameters CLIParametersConfig no inherited
user_timezone str no inherited
additional_directories list[str] no [] (workspace-only) Does not inherit from main. Admin grants per-agent paths (see Sub-agent sandbox).

"inherited" means the value comes from the main agent's config.json when omitted.

Timeout nuance:

  • SubAgentConfig currently has no dedicated timeouts field.
  • SubAgentConfig currently has no dedicated tasks field.
  • sub-agents inherit the main agent timeouts block through merge base.
  • sub-agents inherit the main agent tasks block through merge base.

Example:

[
  {
    "name": "researcher",
    "telegram_token": "123456:ABC...",
    "allowed_user_ids": [12345678],
    "provider": "claude",
    "model": "sonnet"
  },
  {
    "name": "coder",
    "transport": "matrix",
    "matrix": {
      "homeserver": "https://matrix.example.com",
      "user_id": "@coder:example.com",
      "password": "...",
      "allowed_rooms": ["!room:example.com"],
      "allowed_users": ["@user:example.com"]
    },
    "provider": "codex",
    "reasoning_effort": "high"
  }
]

Sub-agent runtime merge behavior

merge_sub_agent_config(main, sub, agent_home) builds the effective sub-agent AgentConfig with this priority:

  1. main agent config (config.json) as base
  2. sub-agent's own config/config.json (if present)
  3. explicit non-null overrides from agents.json (highest priority)

Then it always forces:

  • sygen_home = ~/.sygen/agents/<name>/
  • transport, telegram_token, matrix, allowed_user_ids, and allowed_group_ids from the sub-agent entry
  • api.enabled = false when no explicit api block is provided
  • additional_directories = [] reset before layers 2 and 3 — sub-agents never inherit the main agent's extra paths (see Sub-agent sandbox)

Notes:

  • /model changes in a sub-agent chat are written back to agents.json, so restart/reload uses the updated values from that registry file

Sub-agent sandbox

Sub-agent filesystem access is enforced by two independent layers:

  1. Claude CLI --add-dir — baseline sandbox when the CLI runs in permission_mode="default" / "acceptEdits". The allowed roots are cwd plus the paths in additional_directories.
  2. PreToolUse hook (~/.sygen/agents/<name>/workspace/.claude/ file_sandbox.py, since 1.3.45) — blocks Read, Edit, Write, MultiEdit, NotebookEdit, Grep, Glob calls whose target is outside the sub-agent's workspace plus additional_directories. Bash and other tools pass through so Gradle, Xcode, docker, and git keep working. The hook is required because admin-role sub-agents run with permission_mode="bypassPermissions", which skips the --add-dir check entirely — without the hook, a bypass-permissions sub-agent could read anywhere the user process can.

The hook reads additional_directories directly from ~/.sygen/_secrets/agents.json on every call, so changes from the admin UI take effect on the next CLI invocation with no redeploy. The agent identity comes from the SYGEN_AGENT_NAME env var injected by sygen_bot.cli.executor._build_subprocess_env, so cd into an allowlist directory cannot disable the sandbox.

_ensure_sandbox_hook in sygen_bot/workspace/init.py installs the hook on every init_workspace() call for any path of the form ~/.sygen/agents/<name>/workspace/. The main agent (~/.sygen/workspace/) is skipped — it runs as the admin user with full file access.

For the main agent, the CLI sandbox is sygen_home + config.additional_directories, which typically includes the Sygen source checkout and the admin panel.

Sub-agents are untrusted delegates: each gets its own ~/.sygen/agents/<name>/ workspace and nothing else.

  • Default: additional_directories = [] (workspace-only).
  • Main's additional_directories is not inherited by sub-agents.
  • Admin grants extra paths per sub-agent via:
  • the admin panel (Agents → select agent → Sandbox), or
  • editing the additional_directories field on the agent's entry in ~/.sygen/_secrets/agents.json, or
  • the sub-agent's own config/config.json (useful when the sub-agent runs on a different host that was provisioned separately).
  • Sub-agents cannot extend their own sandbox from inside a chat session. They can ask the main agent or the admin; granting is a privileged action.

REST API: PUT /api/agents/{name} with body {"additional_directories": ["/abs/path", ...]} (admin-only). Paths are validated (absolute, must exist as directories) and de-duplicated.

agents.json watcher behavior

AgentSupervisor watches agents.json (mtime poll every 5s):

  • new entry -> start sub-agent
  • removed entry -> stop sub-agent
  • restart triggers for running agents:
  • transport changed
  • Telegram identity changed (telegram_token)
  • Matrix identity changed (matrix.homeserver or matrix.user_id)
  • other field changes currently do not auto-restart running agents

For non-token field updates on a running agent, use /agent_restart <name> (or restart the bot) to apply them immediately.

Upgrading from pre-1.3.14

Version 1.3.14 landed a security hardening pass that changes defaults a pre-1.3.14 install was silently relying on. After pip install -U sygen (or /upgrade), walk through the steps below before restarting the bot. Skipping them typically manifests as: "Claude CLI approval popups for every file edit and shell command, but there is no popup UI in Telegram."

1. Secrets moved to ~/.sygen/_secrets/

Files that used to live directly in ~/.sygen/ now live in ~/.sygen/_secrets/ (mode 0700, files 0600):

  • users.json
  • agents.json
  • main_agent_token.json

The framework reads both locations during the 1.3.x line — the legacy path keeps working. But all new writes go to _secrets/, so if you have tooling that edits ~/.sygen/users.json directly, point it at ~/.sygen/_secrets/users.json or use the admin API.

2. Telegram users need telegram_user_ids mapping

The CLI permission mode is now chosen from permission_mode_by_role:

"permission_mode_by_role": {
  "admin": "bypassPermissions",
  "operator": "acceptEdits",
  "viewer": "default"
}

For a Telegram message, the bot looks up the sender's numeric from_user.id in every record's telegram_user_ids list in users.json. If no record matches, the role resolves to None and the CLI is started with permission_mode="default" — Claude will ask for approval on every write and tool call, and Telegram has no UI to answer those prompts, so the session hangs.

Fix: make sure every human who talks to the bot has a record in users.json whose telegram_user_ids contains their Telegram ID. The admin panel's Users page exposes this field (since admin 0.3.3); or edit ~/.sygen/_secrets/users.json directly.

Tip: send a message to the bot from a machine you can tail logs on — the bot logs the unknown sender's ID on the first message, which you can then paste into the admin form.

Fresh installs from 1.3.23+: the bootstrap admin's telegram_user_ids is seeded automatically from config.allowed_user_ids on first run, so the setup wizard's single "your Telegram ID" prompt now populates both ACL gates. Upgrades from earlier versions still need a one-time manual edit if the admin record was created before this behavior existed.

3. Inter-agent calls require caller_token

Each sub-agent's record in agents.json gained an interagent_token field. The Sygen CLI writes it automatically when a sub-agent is created after 1.3.14; for older sub-agents the framework still accepts token-less requests but logs a warning ("running in legacy mode"). You can regenerate the token by recreating the sub-agent, or edit _secrets/agents.json by hand and add a 32+ char random string as "interagent_token".

4. The --user-id flag is gone

ask_agent.py and ask_agent_async.py no longer accept --user-id — the caller identity is taken only from the SYGEN_CALLER_USER_ID env var, which the framework sets from the authenticated transport. Passing the flag now exits with code 2 and a message explaining the change. Cron scripts and workflow steps that pass --user-id must be edited. Rationale: the flag let a prompt-injected LLM tool call impersonate any user and bypass per-user ACLs.

5. Additional directories for the Claude sandbox

If Sygen now lives at a non-default path (e.g. you develop it outside ~/.sygen/), add it to cli.additional_directories so Claude can read files from the source checkout:

"cli": {
  "additional_directories": [
    "/Users/you/Agents/sygen",
    "/Users/you/sygen-admin"
  ]
}

Validation rejects relative paths and non-existent directories at startup — you'll see a clear error rather than a silent bypass.

6. Sub-agents are now workspace-only by default

Starting with the current release, sub-agents no longer inherit the main agent's additional_directories. On upgrade, every existing sub-agent is silently narrowed to its own ~/.sygen/agents/<name>/ workspace. No config migration is required.

If an existing sub-agent genuinely needs access outside its workspace (for example, a "builder" agent that touches the Sygen source), grant the paths explicitly:

  • Admin panel → Agents → open the agent → Sandbox → add the path and save
  • Or edit additional_directories on the agent's entry in ~/.sygen/_secrets/agents.json.

The main agent is unaffected — its sandbox continues to come from config.json and still covers the full set of admin-granted paths.