If you discover a security vulnerability in Genesis, please report it responsibly:
- DO NOT open a public GitHub issue.
- Use GitHub Security Advisories to create a private report, or contact the project maintainers directly.
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We aim to acknowledge security reports within 48 hours and provide a substantive response within one week.
| Version | Supported |
|---|---|
| v3.x | Yes |
| < v3 | No |
Genesis v3 is a ground-up rebuild. Earlier versions (v1/v2) are not maintained and should not be used.
Genesis is an autonomous agent system. The same principle that governs its autonomy also governs its security: trust is earned through verified behavior, not assumed.
An autonomous system that can act on your behalf must also be a system you can trust with increasing responsibility over time. The security model is not a lockdown bolted onto an agent -- it is the autonomy model itself. Every layer described below exists because Genesis takes seriously the question: "what has this system demonstrated it can be trusted to do?"
Genesis implements a graduated autonomy framework. The shipped ladder is
four levels, L1-L4 (genesis.autonomy.types.AutonomyLevel); the design
document describes an eventual L5-L7, which is not built. Higher levels
are unlocked through demonstrated competence, verified by the system and
approved by the operator.
Each autonomy level gates specific capabilities:
- L1: Simple tool use — fully autonomous.
- L2: Known-pattern tasks — mostly autonomous.
- L3: Novel tasks — propose and execute with a checkpoint.
- L4: Proactive outreach — threshold-gated and governed.
Autonomy permissions are stored per-category and can be revoked instantly.
There are four categories (genesis.autonomy.types.AutonomyCategory):
direct_session, background_cognitive, sub_agent, outreach. The operator
always has final authority. See
docs/architecture/genesis-v3-autonomous-behavior-design.md for the full
framework, and read it as design intent rather than as a description of what
currently ships.
The autonomous-CLI approval gate is the hard boundary underneath all of it.
Every autonomous background Claude Code session must be rooted in an explicit
operator approval: manual_approval_required (autonomy/cli_policy.py)
defaults to True, and AutonomousCliApprovalGate refuses to dispatch without
one. No autonomy level unlocks past it.
It is a SETTING, not a constant, and you should treat it as one worth checking.
It is read from config/autonomous_cli_policy.yaml and can be overridden by the
GENESIS_AUTONOMOUS_CLI_APPROVAL_ENABLED environment variable — so it can be
turned off in an install without anyone forking anything. The project treats
keeping it True as non-negotiable and ships it that way; what this document
cannot tell you is whether it is still True in YOUR install. Verify it, and
treat a False as the whole earned-autonomy model being switched off.
Genesis is designed to run in an isolated environment (container or VM). This is not optional hardening -- it is the assumed deployment model. The container boundary limits blast radius: even if an autonomous action goes wrong, the damage is contained to the Genesis environment.
Recommendations:
- Run Genesis in a dedicated container or VM, not on a shared workstation.
- Use a non-root user account.
- Restrict network egress to required endpoints (LLM APIs, Qdrant, Ollama).
Genesis exposes a local dashboard over HTTP (and, optionally, a remote-desktop
/ noVNC console). So it can be reached through a reverse proxy or a private
overlay network, the bundled service unit binds the dashboard to all interfaces
(0.0.0.0) by default rather than to loopback. When a dashboard password is
set, state-changing /api requests are gated: check_api_mutation_auth
(dashboard/auth.py) runs as an app-level before_request and requires either
the internal bearer token or an authenticated same-origin cookie, with CSRF
checked from Sec-Fetch-Site/Origin/Referer and failing closed. /v1
enforces its own separate bearer.
Read the limits of that gate carefully, because they decide whether network isolation is still load-bearing for you — it is:
-
It covers mutations only.
GET/HEAD/OPTIONSstay open by design, so health probes and dashboard polling keep working. Anything readable through the API is readable by anyone who can reach the port. -
It is inert when no dashboard password is set, which is the default.
A narrow exception to that last point. A few routes decide not whether to answer but how much to include, and those read
has_verified_credential()instead — which, unlikeis_authenticated(), is never true when no password is configured, because nothing can be proved without a credential to present. Today that covers the provider-key values (/api/genesis/secrets) and the backup target (/api/genesis/backup/status) and the filesystem paths and NAS username (/api/genesis/backup/config). Those are withheld from an unauthenticated caller on every install, including a passwordless one. The routes still answer, but a withheld value changes the response's shape: the secrets route returns each key with an empty value andvalues_withheld: true,/api/genesis/backup/statusomitsdestinations.tier2.target, and/api/genesis/backup/configomitslocal_path,nasandnas_user.This is not the whole class, and it should not be read as one. The carve-out is a list of FIELDS — not a property of a route, a helper, or the shape of a response. Three shorter rules were tried here and all three were false, so the list is given explicitly:
- the
valueof every key in the secrets registry (_KEY_REGISTRY, parsed fromsecrets.env.example), which is the whole file rather than a sensitive subset; - the backup tier-2 target;
- the backup local path, share and username.
Two properties of that list are easy to get wrong. It nests rather than partitions: the backup path, share and username are themselves registry keys, so they appear in the first set as well, reached by a second route. And the same underlying value can be treated differently by each route that serves it — the backup config route strips credentials out of the repository URL before returning it, while the secrets registry returns that key's raw value, so a URL with an embedded token is disclosed there and not here. Read the route, not the field name.
For every field not in that list, assume the default posture above: reads are open,
is_authenticated()opens up when no password is configured, and network isolation is the control. Do not infer that a route is covered because its response resembles one that is — a route elsewhere can mask values behind an explicit reveal step and still sit outside this entirely, and a route whose entire product IS the stored value has no middle state to offer, so gating it would remove the function rather than narrow it. - the
-
It exempts everything under the
/api/genesis/auth/prefix — a prefix match, not a fixed list, so any route added there in future is exempt by default. Today that prefix holds login, logout and an auth-status probe. None of them changes a credential — but login and logout do establish and tear down an authenticated session, and they run ungated by design, because a login that required prior authentication could never succeed. -
It can be disabled outright with
GENESIS_DASHBOARD_API_AUTH=off. That also reopens the file browser's write, create, rename, delete and upload routes to an anonymous caller, inside the browser's allowed roots, which include the Genesis source tree and~/.claude. The file browser's reads stay behind login whatever the switch says. -
It covers state-changing HTTP requests under the API prefixes, and nothing else. The interactive terminal runs over a WebSocket and the noVNC console over its own port; neither is behind this gate, so for those the control is still network isolation and the dashboard password on the UI.
This is safe only under the assumed deployment model: the host is not publicly exposed. The dashboard is meant to be reachable through one of:
- a private overlay network (e.g., Tailscale / WireGuard), where only your own devices can reach the port; and/or
- a host-side reverse proxy that forwards to the container.
The threat model is public exposure — not your LAN or private overlay. If you run Genesis on a host reachable from the public internet, you must restrict the management ports yourself; Genesis does not assume an authenticating gateway in front of them.
Operator checklist:
- Do not port-forward the dashboard or console ports from a public router.
- Bind or firewall the management ports to your private/overlay network — e.g.
restrict the dashboard port to your overlay's address range (Tailscale uses
the
100.64.0.0/10CGNAT range) withnftables/ufw, or change the service unit to bind a specific private interface instead of0.0.0.0. - Treat the built-in web terminal and the noVNC console as unauthenticated administrative access: anyone who can reach those ports can drive Genesis. For the dashboard API, assume the same for reads and for any install with no dashboard password set — with the credential-gated field values noted above as the one carve-out, which is a narrow one and does not change the posture. Network isolation remains the primary control; the mutation gate above is a second layer, not a replacement for it.
Security audits should verify this network restriction (firewall / overlay)
rather than re-flagging the 0.0.0.0 bind, which is intentional for the
proxy/overlay deployment model.
Genesis uses PreToolUse hooks to enforce tool-level security policies at runtime. These hooks fire on every tool invocation, including autonomous sessions, and cannot be bypassed by the agent.
Examples of enforced policies:
- Blocking shell commands that match dangerous patterns (e.g.
rm -rf /) - Blocking destructive git operations, pushes and merges that have not met the repository's review gates, and writes to protected paths
- Blocking web fetches to known-problematic URLs
- Blocking editable installs pointed at a worktree, which would redirect system-wide imports
Hooks are configured in .claude/settings.json, and which hook fires is
decided by the tool matcher -- they are not one program:
Bashis matched by a family of dedicated guards underscripts/hooks/(destructive_command_guard.py,git_discard_guard.py,protected_paths_guard.py,git_push_guard.py,worktree_cwd_guard.py, and others) plus a small inline matcher.WebFetch/WebSearchis matched byscripts/hooks/web_tools_gate.py.Write/Editis matched byscripts/behavioral_linter.py, which lints file content againstconfig/behavioral_rules/*.yaml. It never sees a shell command or a URL.
They are the inner guardrail -- the last line of defense when autonomy permissions have already been granted.
Not every safety mechanism is a hook, and the distinction is a real difference
in guarantee. Process-group kill validation, for example, lives in
genesis.util.proc_kill and hardens Genesis's own subprocess management at
runtime; no hook inspects agent-authored code for it. A hook cannot be bypassed
by the agent; a runtime helper only protects the call sites that use it.
Genesis uses an environment-file approach for secrets:
- All API keys and tokens live in
secrets.envat the project root. - This file is gitignored and should be set to mode
0600(owner read/write only). - The
genesis.envmodule (src/genesis/env.py) resolves the secrets path at runtime, with support forSECRETS_PATHenvironment variable override. - A
detect-secretsscan and a blockinggitleaksscan (.gitleaks.tomlrules: API keys plus install-specific IP/hostname patterns) run in CI on every PR to verify no secrets leak into the public repo. - Scripts read
secrets.envas data (scripts/lib/load_secrets.shor the Python dotenv reader) — neversourceit; a sourced value containing$(...)would execute.
Rules:
- Never commit API keys, tokens, or credentials to version control.
- Never hardcode secrets in source files.
- Rotate keys regularly. Use separate keys for development and production.
Genesis stores episodic memory and knowledge in Qdrant vector collections. A delete guard in the collections module prevents accidental bulk deletion of production data. This was implemented after a real incident where test execution deleted production memory.
The guard:
- Blocks collection-level delete operations unless explicitly overridden.
- Ensures test fixtures use isolated collections that do not collide with production data.
Genesis ingests content it did not author -- fetched pages, mail, documents, third-party messages. The risk is not that this content is stored; it is that stored content is later auto-consumed into privileged state, where it becomes instruction rather than data.
Provenance stamping. Every stored memory and observation carries an
origin_class -- owner, first_party, or an external/untrusted class.
Two privileged-write paths are gated on it, fail-closed on NULL or an
unrecognised value (genesis.security.immunity.is_trusted_for_privileged_write):
- the user model, which will only fold in deltas from trusted origins
(
memory/user_model.py), and - the autonomy dispatcher, which will not pick up a
task_detectedobservation from an untrusted origin (autonomy/dispatcher.py).
Scope this claim precisely: those two paths are gated, not "all untrusted content is isolated." External-origin content can still reach a model's context through ordinary recall and summarisation surfaces. What the gate prevents is untrusted content auto-promoting itself into the user model or into an autonomous dispatch without an operator in the loop.
Irreversible memory operations require approval. Entity merges delete
mentions and links and cannot be un-merged, so they are no longer applied on
staleness alone: the applier consumes only approved proposals
(memory/entity_adjudication.py), and a pre-delete snapshot is journaled so an
applied merge can be reconstructed.
Session identifiers are validated before use as path components. Session ids
arrive from outside the process and several hooks interpolate them into
filesystem paths. A shared validator (is_safe_session_id in
scripts/hooks/hook_input.py) rejects traversal shapes, separators, null bytes,
the empty string, and over-long values. It exists because hooks had each
hand-rolled their own check and disagreed about what was unsafe; adoption is
widespread but not yet complete, so a hook that has not moved over still carries
a narrower check of its own.
Delivery to the operator -- Telegram, voice, mail addressed to you -- is never gated. You are the recipient; gating it would only obstruct you.
Autonomous egress to the outside world is a different matter, and the honest current state is mixed:
- Enforcing: mail sending passes a real gate that can hold a send
(
autonomy/email_gate.py). - Observe-only: the capability gate at the Discord and GitHub-issue doors
(
autonomy/shadow_gate.py) records what it would decide and does not hold anything. Its own contract says so. Treat it as instrumentation ahead of an enforcement stage, not as a control that is protecting you today. - Build-time backstop:
scripts/check_external_io.pyruns in CI and fails the build when a new hardcoded external endpoint appears outside an allowlist. It reasons about literal endpoints in source, so it cannot see egress routed through a browser session or a third-party integration layer.
If you are deciding whether to trust this system with an outbound channel, the first two bullets are the ones that matter: one channel enforces, the rest are watched.
Python dependencies are declared in pyproject.toml (the sole dependency
source — there is no requirements.txt). Known-CVE scanning runs automatically:
- CI — the
dependency-auditjob in.github/workflows/ci.ymlrunspip-auditagainst the resolved runtime tree on every PR/push and weekly, reporting advisories outside its retained ignore baseline in the job log. Audit findings are advisory: step-levelcontinue-on-errorprevents them from failing the job. Dependency installation failures still fail the job. Existing ignored advisories are listed with rationale in that job. - Dependabot — GitHub's dependency-graph security alerts are enabled for
the repo, and
.github/dependabot.ymlkeeps the GitHub Actions current.
To scan locally:
pip install pip-audit && pip-auditIf you suspect a security issue:
- Revoke any compromised API keys immediately.
- Review logs for unauthorized actions or unexpected tool calls.
- Check for unexpected file modifications in the Genesis directory.
- Rotate all credentials.
- Delete
~/.genesis/flask_secret_keyand restartgenesis-server. This is what invalidates existing dashboard sessions — changingDASHBOARD_PASSWORDalone does not. Sessions are signed with that key rather than with the password, so a stolen cookie survives a password change and keeps whatever the session reaches, including the web terminal. - Report the incident to project maintainers.
See LICENSE file for details.