Architecture Decisions — Horizon AIOS
This document memorializes the "why" behind structural decisions made as Horizon AIOS is built. It is a living document — new decisions are appended as they are made. Existing entries are never deleted or revised; if a decision is superseded, a new entry records the reversal and its rationale.
How to use this document: When making a structural or design decision, append a new entry to the Architectural Decisions Log (Section 3) with the current date, a short title, what was decided, why, and any implications for future work.
1. Purpose
Architecture decisions accumulate silently. Six months into a project it becomes impossible to reconstruct why a particular structure was chosen, and the team (human or AI) makes decisions that unknowingly conflict with prior constraints. This document prevents that.
Every time a non-obvious structural choice is made — about file placement, sync strategy, naming convention, permission model, or anything else that affects how the system fits together — an entry is added here. The "why" is more important than the "what"; the what is visible in the files themselves.
2. File Synchronization Manifest
This section is the authoritative reference for which files have a source-of-truth location, which have a deployed/machine-local copy, and how they are kept in sync.
2.1 Synced Pairs (source → deployed copy)
| File / Directory | Source (in repo) | Deployed / Machine-Local | Sync Mechanism | In Git Repo |
|---|---|---|---|---|
| CLAUDE.md instructions | $HORIZON_ROOT/.claude/CLAUDE.md |
~/.claude/CLAUDE.md |
One-time stub creation (@ redirect, not a copy) | Source only — stub is machine-local |
| settings.json template | $HORIZON_SYSTEM/templates/claude_code/settings.json |
~/.claude/settings.json |
Copy template + substitute AIOS_EXEC_WRAPPER (bootstrap §5 / horizon_aios_switch.py init) |
Template only — local copy is NOT committed |
Note on settings.json: The deployed ~/.claude/settings.json is a copy of the template with the single AIOS_EXEC_WRAPPER placeholder substituted to the machine-local aios-exec wrapper (~/.horizon/bin/aios-exec.{ps1,sh}). It is AIOS-independent — the wrapper resolves the active AIOS at run time — so it is not rewritten on aios switch. The bootstrap script (Section 5) does the substitution. See $HORIZON_DOCS/getting_started/ReadMeToSetupYourSystem.md §6.2 and $HORIZON_DOCS/system/aios_switching.md.
Note on skills: Skills are no longer synced via copy. ~/.claude/skills/ is a symlink pointing directly to $HORIZON_SYSTEM/skills_sbin/ for the primary user, and to $HORIZON_SYSTEM/skills_bin/ for brain users. Changes to skills in the repo are live immediately — only a Claude Code session restart is needed. See the 2026-06-21 ADR entry for the skills redirect decision.
2.2 Local-Only Files (not in repo, not synced)
These files exist only on the local machine. Anyone who clones the repo will not have them — they must be created during setup.
| File | Location | Why Local-Only |
|---|---|---|
| Global Claude settings | ~/.claude/settings.json |
Contains machine-specific paths; configured from template at setup time |
| CLAUDE.md stub redirect | ~/.claude/CLAUDE.md |
One-line @ pointer to repo CLAUDE.md; machine-local bootstrap artifact |
| Local git config | $HORIZON_ROOT/.git/config |
Git-managed, machine-local (e.g., core.hooksPath setting) |
| Brain-specific configs | $HORIZON_ROOT/brains/<brain_name>/ |
Per-brain, scoped to brain user; sensitive; gitignored |
| Handoff documents | $HORIZON_ROOT/handoffs/ |
Session-specific artifacts; machine-local continuity; gitignored |
2.3 Repo-Tracked Files (synced via git clone/pull)
These files are committed to the OS repo and present on every machine after cloning.
| File / Directory | Path | Notes |
|---|---|---|
| Canonical AI instructions | $HORIZON_ROOT/.claude/CLAUDE.md |
System-level instructions for all Claude sessions |
| Devroot permissions | $HORIZON_ROOT/.claude/settings.json |
Devroot-scoped tool permissions only (no hooks, no statusLine) |
| Skills (primary user) | $HORIZON_SYSTEM/skills_sbin/ |
Live via symlink at ~/.claude/skills/ → no copy needed |
| Skills (brain users) | $HORIZON_SYSTEM/skills_bin/ |
Live via symlink at brain ~/.claude/skills/ → no copy needed |
| Sounds (generic) | $HORIZON_SYSTEM/sounds/*.wav |
Vendor-agnostic event audio |
| Sounds (vendor-voiced) | $HORIZON_SYSTEM/sounds/<vendor>_event_sounds/ |
Vendor-specific voiced audio |
| Statusline scripts | $HORIZON_BIN/statusline/ |
Terminal statusline scripts for Claude Code |
| Harness configs | $HORIZON_SYSTEM/harness_configs/ |
Per-harness config templates (git hooks, etc.) |
| Config templates | $HORIZON_SYSTEM/templates/ |
Setup templates; contain placeholders, not real paths |
| Invariant documents | $HORIZON_ETC/ |
File structure, security, and personalization invariants |
| Documentation | $HORIZON_DOCS/ |
User-facing docs including this file |
| Privileged scripts | $HORIZON_SYSTEM/sbin/ |
Owner-only; brain users may not access |
| Gitignore | $HORIZON_ROOT/.gitignore |
Repo-wide ignore rules |
3. Architectural Decisions Log
Entries are in reverse-chronological order at the top (newest first). Each entry is immutable once added.
2026-07-24 — Brains-group ancestor traverse: traverse-only right + generic doctor ancestor-walk check
Status: Accepted. Owner: HorizonBrute (package developer).
Refines: the 2026-07-19 "ACL posture externalized to config..." entry below — the abstract rights vocabulary introduced there gains one new primitive (traverse-only); the shared-loader / single-source-of-truth contract between harden.py and doctor.py is unchanged and is what makes the new doctor check possible without a second hardcoded path list.
Context. brains held read-exec on $HORIZON_SYSTEM/bin and skills_bin, but no rule granted the group anything on $HORIZON_ROOT or bare $HORIZON_SYSTEM — recursive/default only project a rule DOWN its own subtree, never UP to ancestors, so those two leaf grants were unreachable by anything relying purely on the declarative rule set. The gap was masked by two things that made it invisible: a Linux-only, rule-set-external function (linux_traverse_ops) hardcoded to assert --x on the root and the brains/ parent directly (predating the TOML externalization below and never promoted into it), and the brains-system-nowrite deny rule's POSIX realization as r-x (POSIX ACLs have no true deny, so "no write" is modeled as "read intact"), which incidentally covers $HORIZON_SYSTEM itself as a side effect of a rule whose stated intent is write-denial, not traverse-grant. Neither mechanism is expressed in, or verified against, the posture — so horizon_aios_doctor.py had no way to catch a regression in either, and neither existed at all on Windows/macOS.
Decision A — Add traverse-only (--x) as a first-class right. The abstract rights vocabulary gains a pure execute-only primitive, distinct from create-traverse (-wx, write+enter, used for the human self-service parents): enter a directory, no read/list, no write. Three new rules use it — brains-root-traverse ($HORIZON_ROOT), brains-system-traverse ($HORIZON_SYSTEM), brains-brains-parent-traverse ($HORIZON_ROOT/brains) — each non-recursive and non-default (not inherited onto new siblings created later). All three translators gained a mapping: Linux --x (falls through the existing generic standard-rights path), macOS execute (same generic path), Windows a bare granular (X) with no (OI)(CI) (a dedicated branch, mirroring the self-service parent's bare-granular convention — the generic Windows grant masks are always-inheritable and would wrongly cascade X onto the whole subtree).
Decision B — Doctor gains a generic ancestor-traversal check, not a hardcoded one. Posture.required_ancestor_traversal() (in horizon_aios_acl_posture.py, the shared loader) walks every non-deny grant rule's path up to $HORIZON_ROOT, one ancestor at a time, for that rule's own principal, and returns the (path, principal) pairs that must carry at least execute. horizon_aios_doctor.py asserts each pair via getfacl (Unix) / Get-Acl (Windows). Because the derivation is generic over ANY grant rule (not a brains-specific hardcoded list), a future leaf grant added without its own ancestor rule fails doctor loudly instead of silently regressing exactly like this one did.
Rationale: Promoting the ad-hoc, Linux-only, rule-set-external traverse assertion into a declarative rule closes the Windows/macOS gap (they had no traverse mechanism at all), makes the requirement machine-checkable by the same shared loader harden.py and doctor.py already share, and removes the accidental dependency on no-write's POSIX side effect. linux_traverse_ops is left in place as a harmless, idempotent belt-and-braces re-assertion of the root/brains/ subset — removing it was not required to close the gap and would drop the only traverse mechanism for a deployer whose .local.toml disables the new rules without realizing they were reachability-critical.
Implications:
file_acl_hardening.toml's "brains group" section now opens with three ancestor-traverse rules before the leaf grants; the rights-vocabulary schema comment documentstraverse-onlyalongside the others.security_architecture_invariants.md's brains-grant table states the invariant explicitly: a grant on a leaf requires traverse on every ancestor; traverse is not read.horizon_aios_harden.py's additive-Linux and Windows paths pick up the new rules automatically (they already loop the loaded posture generically forprincipal == brains); the strict/no-setfacl-fallback Linux path gained an explicit loop since it dispatches brains grants by hardcoded chown/chmod steps rather than the generic per-rule translator.- Any future area a brain is granted a leaf right on inherits this protection for free — no doctor update needed, unless the new leaf sits somewhere the generic ancestor walk cannot reach (e.g. a per-principal home directory managed by ownership rather than by a rule, as
brains/<own-name>/already is).
2026-07-20 — Human projects/ isolation: mount-view rejected; self-healing harden + land-in-own-dir on the self-service model
Status: Accepted. Owner: HorizonBrute (package developer).
Refines: the two 2026-07-19 entries below. The self-service isolation guarantees (-wx create-traverse parent, born-owner-only children, per-area shared/ drop-zone) are UNCHANGED. This entry records (A) why a per-user mount view was rejected in favour of the POSIX home-directory model, (B) that horizon_aios_harden.py now ASSERTS & REPAIRS the posture on every run rather than only applying it, and (C) the per-user provisioning + landing affordance that makes the "you cannot ls the parent" cost (2026-07-19 Implications #1) ergonomic without weakening isolation. It does NOT return to the pre-9a26089 admin-precreate model — self-service creation via -wx still stands; provisioning is additive convenience.
Context. A human could write into projects/ but got Permission denied on ls of the root — correct by design (horizon_humans has -wx, no read). The intent "let me ls and see only MY folders, never anyone else's, with zero metadata leak" runs into a hard POSIX constraint: directory read is all-or-nothing over entries — a listing cannot be filtered by per-entry ownership. The three-way tradeoff (shared dir listable at its root / see only yours / zero leak) admits at most two; only a per-user mount view satisfies all three.
Decision A — Reject the mount-view; keep the POSIX home-directory model. A per-user pam_namespace / bind-mount view of projects/ (so the root resolves to only the caller's subtree) was considered and rejected:
- No clean cross-platform parity — Windows has no
pam_namespace; matching it would need per-user junctions / FSLogix-style machinery, breaking the single-posture, three-OS symmetry the ACL engine is built on. - Operational risk — it depends on a login-time mount (
session required pam_namespace.so); a misconfigurednamespace.confcan block ALL logins, including console. - The home-directory model already delivers the intent with pure ACLs: each human's working root is their own owner-only
projects/<user>(theylsthat, likels ~, never the shared parent). Zero metadata leak, identical on Linux/macOS/Windows. Traverse (x) on the parent does not expose siblings — it only resolves a path to the caller's own dir; each sibling is independently owner-only, and listing still needs read, which the group never gets.
Decision B — harden is self-healing: assert & repair, not just apply. Previously horizon_aios_harden.py applied grants blindly and only horizon_aios_doctor.py READ and reported drift (never repairing). A new assert_repair_selfservice pass now runs AFTER the grant/deny loops on all three OSes:
- Parent: STRIP everything not in the config, then re-establish exactly the configured entries — Linux
setfacl -b, macOSchmod -N, Windowsicacls /reset+/inheritance:r— so a stray named-principal ACE (e.g. someone addingu:x:r-xto the root) cannot survive. The shipped-wx+ isolating default are re-pinned;other::---clamped; sticky preserved. - Children: each existing
projects/<user>forced back to owner-only (setfacl -R -b+chmod 700;icaclsowner/SYSTEM/Administrators + OWNER RIGHTS,horizon_humansremoved), repairing a drifted mode (chmod 755) or an added group ACE.shared/and anyexclude_childrenare excluded.
doctor still verifies; harden now also corrects, so the two never diverge and drift self-heals on every onboarding + nightly maintenance run.
Decision C — Per-user provisioning parity + land-in-own-dir. So a human lands where ls shows only their own entries:
- Per-user
projects/<user>is provisioned owner-only at enrollment and RE-ASSERTED every run (self-heals drift).bootstrap.shalready did this on Linux;bootstrap.ps1was MISSING it entirely and now provisions withicaclsparity (break inheritance; Full to owner + SYSTEM + Administrators via well-known SIDs; nothing tohorizon_humans). - An idempotent, interactive-only, fail-soft landing hook (
~/.bashrcmanaged block on Linux; PowerShell profile managed block on Windows, sentinel-guarded) starts a human's interactive shell in their ownprojects/<user>if not already under it. Non-interactive shells, missing dirs, and unresolved profiles all fail soft and never abort enrollment.
Finding (Windows latent inherited read — follow-up). The root humans-userspace-full grant is horizon_humans (OI)(CI)F inheritable at $HORIZON_ROOT, which inherits READ down onto projects/; the this-folder-only (WD,AD,X) create-traverse grant cannot subtract an inherited allow. Both the apply path and the new repair close this by breaking inheritance (/inheritance:r) at the parent, reaching no-read parity with Linux -wx without an explicit Deny. A permanent defense-in-depth fix — an explicit no-read posture on projects/ itself, or narrowing the root grant — is deferred as a follow-up so the guarantee does not depend on the repair having run.
Implications:
- Isolation guarantees are unchanged; humans still cannot
lsthe shared parent (by design) — the landing hook makes that ergonomic rather than a friction point, and the shared parent stays non-listable. - Drift in the self-service posture now self-heals on every
hardenrun;doctorremains the read-only verifier. - The mount-view option is closed; revisiting it would reopen the cross-platform-parity and login-lockout costs recorded here.
- Windows carries a tracked follow-up for the latent inherited-read hole above.
2026-07-19 — ACL posture externalized to config + full cross-platform (Linux/macOS/Windows) parity
Status: Accepted. Owner: HorizonBrute (package developer).
Refines: the 2026-07-19 "Self-service per-user isolation + per-area shared/ drop-zone on the four human shared areas" entry (immediately below). That entry defined WHAT the posture guarantees on the four human areas; this entry records WHERE the posture now lives (an externalized, deployer-customizable config) and that all three OSes now implement it from that single source. The guarantees themselves are unchanged.
Decision A — Externalize the whole ACL posture to a deployer-customizable config. The complete hardening stance — previously hardcoded in horizon_aios_harden.py — is now declared as abstract-intent rules in a git-tracked TOML file, deep-merged with a gitignored local override:
horizon_system/ai_os_etc/file_acl_hardening.toml— the shipped default (git-tracked). Encodes the entire posture: humans full / system-read-only / canon-read-only / brains-read-write, the four self-service areas, each area'sshared/drop-zone, brains read-exec onbin/skills_bin, brains deny onsbin/skills_sbin/logs, and the system-wide no-write catch-all. Rules are ABSTRACT INTENT, never platform syntax; the schema notes at the file's foot are the authoritative field/rights vocabulary.horizon_system/ai_os_etc/file_acl_hardening.local.toml— a GITIGNORED deployer override, deep-merged over the default keyed by rule/groupname(local wins: a matching name replaces that rule's fields, a new name adds a rule,disabled = truedrops a rule). It ships asfile_acl_hardening.local.toml.template. It is sync-safe by construction: the two-lane sync's official-lane hard-restore only reclaims official tracked paths, so the gitignored.local.tomlis never overwritten.horizon_system/sbin/horizon_aios_acl_posture.py— a NEW shared module: TOML loader + deep-merge + a FAIL-SECURE embedded fallback (a missing or corrupt config still hardens from the built-in posture) + the three-OS translator. Bothhorizon_aios_harden.py(apply) andhorizon_aios_doctor.py(verify) consume this one module, so they can never disagree about the posture.
Decision B — Full cross-platform parity driven by the same posture. The translator emits native mechanisms from the same abstract rules on every OS — setfacl (Linux), chmod +a (macOS), icacls (Windows). Windows and macOS now reach parity with the previously Linux-first self-service/isolation model:
- Windows child isolation uses OWNER RIGHTS (
S-1-3-4), never a group Deny — a Deny would lock out the owner, who is itself ahorizon_humansmember. - macOS uses
chmod +aACEs for the same guarantees. - On a non-native host the foreign-OS branch is dry-run only.
Rationale: Hardcoding the posture made every stance tweak a code edit to a privileged script, and the framework-vs-user-space contract forbids local edits to tracked framework files (they are reclaimed on the next official-lane sync). Externalizing to a tracked default plus a gitignored, local-wins override gives deployers a durable, sync-safe customization seam that needs no code change, while the fail-secure embedded fallback guarantees the machine still hardens if the config is missing or malformed. Sharing one posture module between the applier and the verifier eliminates the classic drift where harden.py grants one thing and doctor.py checks another. Abstract-intent rules keep single-OS deployers out of platform ACL syntax and let one declaration harden all three OSes identically.
Implications:
- The stance is customized by editing
file_acl_hardening.local.toml(copied from the.template), never by editingfile_acl_hardening.tomlor the Python — direct edits to the tracked default are reclaimed on the next official-lane sync. - Any future rule / field / rights-vocabulary change lives in the TOML schema and the shared posture module;
harden.pyanddoctor.pyinherit it automatically from the one source. - A genuine platform-specific need uses a per-rule
[rules.raw]escape hatch (verbatim per-OS args), applied at the deployer's responsibility. security_invariants.md,file_structure_invariants.md §14, and this log now describe a config-driven posture; the authoritative field vocabulary is the schema notes infile_acl_hardening.toml.
2026-07-19 — Self-service per-user isolation + per-area shared/ drop-zone on the four human shared areas
Status: Accepted. Owner: HorizonBrute (package developer).
Supersedes: the earlier projects/-only isolation approach (commit 9a26089), which made the projects/ parent traverse-only (--x, no write) and therefore BLOCKED self-service creation — a user's projects/<user> folder had to be pre-created by an admin (bootstrap --add-human). This entry reverses that for all four areas: creation is now self-service, no admin or enrollment tooling required.
Note: This entry records the decision. The horizon_aios_harden.py / horizon_aios_doctor.py code changes that implement and guard the model are being made separately (in progress at time of writing); the ADR is not gated on the implementation status of every check.
Decision A — Self-service per-user isolation on shared human areas. The four human-facing shared areas — projects/, handoffs/, objectives/, usrbin/ — adopt a uniform "self-service isolation" model: any member of the horizon_humans group can create their own folders/files directly, while being unable to see, read, enter, or delete another user's entries. On Linux/Unix this is set by setfacl in horizon_aios_harden.py:
- The parent grants
horizon_humans-wx(write + traverse, NO read) — members self-create but cannot enumerate the parent listing. - The sticky bit (
+t) on the parent is mandatory — without it a group member could delete or rename another member's entry in a group-writable dir. - NO setgid — so a newly created entry's group is the CREATOR'S PRIVATE GROUP (e.g.
lvcol:lvcol), satisfying the requirement that both the owner and the group of a creation are the creating user and that user's own user-group. - A default ACL makes every new entry born owner-only:
default:group:horizon_humans:---,default:other::---. Other humans are denied on the entry itself.
Windows parity: broken inheritance plus OWNER RIGHTS (S-1-3-4) on children — NOT a group Deny, because a Deny would lock the owner out (the owner is itself a horizon_humans member).
Decision B — Per-area shared/ drop-zone. Full per-user isolation would prevent genuine cross-user / cross-utility handoffs, so each of the four areas also contains a single shared/ subdirectory that is group-readable/writable by horizon_humans (setgid so shared content is group-owned; sticky so members can't clobber each other's shared files). Default per-user isolation applies to everything else; shared/ is the explicit escape hatch for A→B handoffs between utilities.
Rationale: The prior admin-pre-create model added enrollment friction and a privileged step for what is fundamentally self-service scratch space. A flat shared namespace with -wx (write+traverse, no read) on the parent plus sticky-bit and a born-owner-only default ACL lets each human own their own subtree with zero tooling, while structurally denying visibility into peers' entries. Dropping setgid is deliberate: it is what makes a creation land under the creator's private group rather than a shared group, which is the ownership invariant we want for isolated entries. The single shared/ per area preserves the one legitimate need the isolation would otherwise break — deliberate cross-utility handoff — behind an explicit, group-owned, clobber-safe directory rather than by weakening the default.
Implications:
- Users cannot
lsthe top of these four dirs (no read on the parent) — they must know/keep their own paths. This is the inherent cost of a flat shared namespace with per-entry isolation. - The model is enforced by
horizon_system/sbin/horizon_aios_harden.py(applied by onboarding and nightly maintenance) and guarded byhorizon_system/sbin/horizon_aios_doctor.pychecks. - Windows and Unix diverge in mechanism (OWNER RIGHTS vs. sticky + default ACL) but converge on the same guarantee: a creation is owned by its creator, invisible to peers, and undeletable by peers;
shared/is the sole group-visible exception.
2026-06-23 — Standardized on symlinks across all platforms; junction terminology retired
Decision: The AIOS now uses symbolic links (symlinks) exclusively across all platforms — Windows, macOS, and Linux — for skills discovery (~/.claude/skills/), memory redirect (~/.claude/projects/), and brain .claude home wiring (<brain-home>/.claude). The previous Windows-specific NTFS directory junction approach is retired. All documentation terminology updated from "junction" (or "junction/symlink") to "symlink."
Rationale: NTFS directory junctions are a Windows-only construct; using junction terminology alongside symlink terminology created per-platform documentation branches and reader confusion. Windows supports symbolic links natively (via mklink /D or New-Item -ItemType SymbolicLink); they are functionally equivalent to junctions for directory redirection and do not require administrator rights when Developer Mode is enabled. Standardizing on symlinks unifies the vocabulary, eliminates the dual-terminology burden in documentation, and makes the implementation description accurate across all platforms.
Implications: Any script or tool that previously created NTFS junctions (New-Item -ItemType Junction) should be updated to create directory symlinks (New-Item -ItemType SymbolicLink). Existing junctions on already-deployed machines are functionally equivalent and do not need to be recreated. All documentation (KB, AIOS working copy) updated to use "symlink" uniformly.
2026-06-22 — Distribution & update model: framework vs. user-space
Decision: A downstream install tracks Horizon AIOS as an upstream remote and keeps its own work + backups on its own origin; updates arrive via horizon_aios_sync.py (fast-forward-only) or git merge upstream/<branch>. The repo splits into framework (upstream-owned: horizon_system/**, the tracked .gitignore, templates) and user-space, which upstream never writes: .gitignore.user, aios_local.conf, ~/.horizon/, ~/.claude/, memory/, handoffs/, objectives/, brains/, *.local.*, aios_overrides.md. The contract: customize only through the user-space/override layers, never by editing framework files. User data is backed up to the user's OWN remote via sbin/horizon_aios_backup_user_data.py — it force-adds data to a per-machine branch, never edits the framework .gitignore, and refuses to push to the public upstream. .gitattributes sets merge=union on .gitignore/.gitignore.user so upstream and local ignore additions never conflict.
Rationale: Editing the framework .gitignore to un-ignore data (the obvious but wrong move) makes every upstream update conflict. Force-adding to a separate per-machine backup branch keeps framework history clean and updates conflict-free while still giving cross-machine awareness. Fast-forward-only sync is a guardrail — if the user edited a framework file, the sync refuses rather than silently clobbering. The leak guard prevents the catastrophic case of pushing private transcripts to the public upstream.
Implications: New sbin/horizon_aios_backup_user_data.py + AIOS_BACKUP_* config keys; .gitattributes union-merge; documentation/system/distribution_and_updates.md is the authoritative model. Every future feature must respect the framework/user-space line: a feature that produces user data places it in user-space (gitignored), never tracked in the framework.
2026-06-22 — AIOS switcher: machine-local registry + indirection layer
Decision: A machine can host multiple Horizon AIOS installs and switch which one its local Claude config points at, via horizon_system/sbin/horizon_aios_switch.py and a self-healing named registry at ~/.horizon/aios_registry.json. Two of the five machine-global pointers are decoupled through an indirection layer so a switch is a pointer write, not a re-stamp: env vars are sourced from a generated ~/.horizon/active_env.{ps1,sh}, and ~/.claude/settings.json points once at stable ~/.horizon/bin/aios-exec.{ps1,sh} wrappers that resolve the active AIOS at run time. The CLAUDE.md redirect and skills symlink are rewritten per switch; the sync schedule is advisory.
Rationale: Previously every pointer hardcoded one HORIZON_ROOT, so running a second AIOS meant hand-editing the profile, settings.json, the CLAUDE.md redirect, and the skills symlink. Indirecting the two highest-churn pointers (env + settings.json) means they never change on switch — only a generated file does. A named registry (vs. path-based) suits long-lived AIOSs switched often; it self-heals — any command rebuilds it from the current tree if missing — so a fresh machine never hard-fails.
Implications: New machine-local state lives in ~/.horizon/ (registry, active_env, wrappers), outside $HORIZON_ROOT, never committed or synced. settings.json gains a single AIOS-independent placeholder AIOS_EXEC_WRAPPER (replacing HORIZON_BIN_PATH/HORIZON_SYSTEM_PATH). The profile now sources active_env instead of hardcoding HORIZON_*. Bootstrap Section 5 runs horizon_aios_switch.py init. Switching the operator's config does not touch brain users (pinned per AIOS). A switch requires a Claude restart + new shell (env changes do not reach running sessions). See documentation/system/aios_switching.md.
2026-06-21 — Skills redirect via junction/symlink supersedes copy-based deploy
Decision: Supersedes "2026-06-20 — Skills have dual location: source and deployed copy." ~/.claude/skills/ is now a directory junction (Windows) or symlink (Unix/macOS) pointing directly to $HORIZON_SYSTEM/skills_sbin/ for the primary user, and to $HORIZON_SYSTEM/skills_bin/ for brain users. No copy step exists; skills are live on disk as soon as they are written to the repo directory.
Rationale: The copy-based model required a manual deploy step after every skill change, with no way to detect drift between source and deployed copies. Per-skill symlinks inside ~/.claude/skills/ were considered but rejected as inelegant (one symlink per skill, all pointing into the same source tree). Redirecting the entire directory via a single junction/symlink is simpler, drift-proof, and consistent with how other AIOS config files are managed (the CLAUDE.md stub uses the same pattern). Windows directory junctions require no administrator rights, keeping the bootstrap friction-free.
Implications: Skills are split across two source directories mirroring the bin/sbin security model: skills_sbin/ (primary user only) and skills_bin/ (brain-readable). Bootstrap creates the primary user junction to skills_sbin/; horizon_aios_create_brain.py creates the brain junction to skills_bin/. A session restart is the only action needed after adding or editing a skill. The "deployed copy" concept no longer applies to skills.
2026-06-21 — Docker deployment model: brain isolation via containers, not OS users
Decision: Added Docker deployment templates (horizon_system/templates/docker/): Dockerfile, docker-compose.yml, .dockerignore. Added bootstrap_docker.sh to sbin/ as a thin wrapper around bootstrap.sh that sets AIOS_DEPLOY_MODE=docker (suppresses shell profile instructions and sync schedule setup). Added documentation/deployment/docker.md and documentation/tested_configurations.md.
In the Docker deployment model, brain isolation is container-level rather than OS-user-level. Each brain runs as a separate Docker container. The AIOS OS layer runs in the primary horizon-aios container; brains are defined as additional services in docker-compose.yml with per-brain volume mounts for their directories and keys. The audit log volume is not mounted into brain containers.
Rationale: IaC/containerization compatibility is a first-class design goal (see philosophy.md §5). The native OS user model and the Docker container model are isomorphic at the security boundary level — both use OS-enforced isolation with explicit provisioning of tools and credentials per brain. Docker adds network isolation and makes deployment reproducible and portable. The two models coexist: the Docker templates wrap the native AIOS layer without modifying it.
Implications: horizon_aios_create_brain.py is not yet Docker-aware — it provisions OS accounts, not containers. Brain container provisioning in Docker is currently manual (duplicate the compose service template). This is tracked as a gap in tested_configurations.md. The Dockerfile is harness-agnostic except for Claude Code CLI installation; the BYOH principle is documented in the Dockerfile and docker.md.
2026-06-21 — Formalized Brain vs. AIOS vocabulary; created philosophy.md
Decision: The conceptual distinction between a "Brain" (an atomic expert system — an App) and an "AI Operating System" (the config, harness, security, and logging layer that Brains run on top of) is now formally documented in $HORIZON_DOCS/philosophy.md. The term "Brain" in all AIOS documentation refers specifically to a purpose-scoped agentic workflow running as an isolated OS user account. What the broader industry often calls an "AI OS" (a highly tuned expert system for a specific domain) maps to "Brain" in Horizon vocabulary.
Rationale: The vocabulary was implicit in the implementation but never stated. Contributors, auditors, and new AI sessions had to infer the distinction from context. The gap also meant the "blue team answerability" design goals — what is the agent doing, how is it doing it, how is it enforced, what data/tools does it have access to — were scattered across technical documents rather than stated as a unified design objective. philosophy.md also documents the IaC/containerization deployment model, the BYOH principle, and an evaluation of where the current implementation aligns with or falls short of these values.
Implications: Any document, skill, or AI instruction that introduces a new use of "brain," "AIOS," or "second brain" must be consistent with the vocabulary in philosophy.md. Architectural decisions that affect the Brain/AIOS boundary (e.g., adding capabilities to sbin vs. provisioning them to individual brains) should be evaluated against the conceptual separation documented there.
2026-06-20 — Two-group model per brain (brains common group + brain-specific group)
Decision: Each brain belongs to two groups: brains (common AIOS group, grants read+execute on $HORIZON_BIN) and <brain-name> (brain-specific group, grants full control on the brain's own folder). The invoking primary user is added to the brain-specific group for oversight but is not added to every brain's group.
Rationale: Separates two distinct privileges into two distinct groups. The brains group answers "can this account use shared AIOS tooling?" The <brain-name> group answers "who owns this brain's data?" Adding the primary user to the brain-specific group enables oversight (read/write to the brain folder) without placing the primary user in a catch-all group that spans all brains. This keeps group membership semantics clean and least-privilege.
Implications: The brains group ACL on $HORIZON_BIN must never cascade to $HORIZON_BIN/sbin. The sbin ACL must be explicitly set and verified after any $HORIZON_BIN group change — this is enforced structurally in horizon_aios_create_brain.py (Phase 3 always re-locks sbin last). On Windows, sbin requires an explicit /inheritance:r ACE reset, not just absence of a grant, because inherited permissions take precedence over "no entry."
2026-06-20 — Brain provisioning via Python script (horizon_aios_create_brain.py)
Decision: OS-level provisioning of new brains (user account, groups, folder, permissions) is performed by a single Python script at $HORIZON_SYSTEM/sbin/horizon_aios_create_brain.py, invoked by the primary user with admin/root privileges.
Rationale: Python is cross-platform. A shell script would require separate .sh and .ps1 variants to handle Linux/macOS and Windows, leading to two codebases that can drift. A single Python script with platform branches (platform.system()) keeps provisioning logic in one place, makes cross-platform divergences explicit and auditable, and is readable by any contributor without needing to know both Bash and PowerShell. Python 3.6+ is a reasonable dependency for a developer tooling system.
Implications: Python 3.6+ is a documented dependency for brain provisioning (added to the "Adding a Brain" section of ReadMeToSetupYourSystem.md). The script must be run as Administrator/root. (Update 2026-06-22: originally placed in $HORIZON_SYSTEM/scripts/ to signal explicit, intentional invocation rather than a privileged daemon; later consolidated into $HORIZON_SYSTEM/sbin/ alongside the other privileged admin scripts. It is still invoked explicitly by the primary user; the sbin brains-Deny ACL is appropriate since brains must never run provisioning.) No automatic rollback is implemented by design: partial state is preserved and cleanup instructions are printed so the primary user retains full control over what gets undone.
2026-06-20 — Project overrides via aios_overrides.md at project root
Decision: Projects that need to override AIOS defaults (e.g., handoffs output directory, display name) do so via a file named aios_overrides.md placed at the project root directory — not inside .claude/.
Rationale: Placing the override file at the project root (not in .claude/) makes it easy to discover, not specific to Claude Code, and extensible to other harnesses or tooling that reads the same file. The /handoff skill and other AIOS skills walk upward from the working directory to find it before falling back to AIOS defaults.
Implications: Any AIOS skill that has configurable behavior must check for aios_overrides.md before acting. The template at $HORIZON_SYSTEM/templates/aios_overrides.md is the canonical reference for supported keys.
2026-06-20 — Self-activating handoff document header
Decision: Every handoff document produced by the /handoff skill begins with a block instructing any AI agent reading it to treat it as a directive to begin work, with any accompanying user text as additional instructions.
Rationale: Handoff files need to be directly usable as session starters — paste the file path or its contents and work begins without additional prompting. The self-activating header eliminates the intermediate "explain what to do with this" step. This is a UX property of the artifact, not a technical constraint.
Implications: The /handoff skill template must include the activation header. Agents should be designed to honor it when reading a handoff.
2026-06-20 — Handoffs are session artifacts, gitignored
Decision: $HORIZON_ROOT/handoffs/ is gitignored. Handoff documents are not committed to the OS repo.
Rationale: Handoffs contain session-specific state and potentially sensitive context (partial code, personal workflow notes, in-progress work). They serve as machine-local continuity between sessions, not as version history. Committing them would pollute the repo with ephemeral content and risk exposing session-sensitive information if the repo is shared.
Implications: Handoffs do not survive a fresh clone. Anyone setting up a new machine starts fresh. If cross-machine continuity is needed in the future, it must be implemented as an explicit export/import mechanism, not via git.
2026-06-20 — bin/sbin boundary for brain isolation
Decision: $HORIZON_BIN/ (bin) is group-readable — brain user accounts may read and execute scripts and assets there. $HORIZON_SYSTEM/sbin/ is owner-only — brain user accounts must never have access.
Rationale: Mirrors the Unix /bin and /sbin filesystem convention. Brains need access to shared tooling (sounds, templates, status scripts) without needing elevated authority. Privileged scripts that manage brain accounts or modify OS-level config require primary-user authority. The explicit directory boundary enforces this without needing per-file permission management.
Implications: Any script that requires primary-user authority must be placed in sbin/. Scripts placed in $HORIZON_BIN/ root or subdirectories other than sbin/ must not assume elevated privileges. On Windows, sbin/ requires an explicit Deny ACL for brain users (not just absence of a grant) because inherited permissions can otherwise leak access.
2026-06-20 — ~/.claude/CLAUDE.md is a stub redirect, not a copy
Decision: The machine-local ~/.claude/CLAUDE.md contains a single line: @$HORIZON_ROOT/.claude/CLAUDE.md. It is not a copy of the repo's CLAUDE.md.
Rationale: A single source of truth for AI instructions. If ~/.claude/CLAUDE.md were a copy, updates to the canonical instructions would require a manual copy step and would introduce drift risk. The @ import syntax in Claude Code resolves the redirect at load time, so the deployed stub never needs updating when instructions change.
Implications: New machines only need the stub created once during bootstrap. The stub itself is machine-local and not committed. The canonical instructions live entirely in $HORIZON_ROOT/.claude/CLAUDE.md and are versioned with the repo.
2026-06-20 — Skills have dual location: source and deployed copy
SUPERSEDED by 2026-06-21 — Skills redirect via junction/symlink. The copy-based model below is no longer in use.
~/.claude/skills/is now a directory junction/symlink pointing directly at the source. No deploy step exists.
Decision: AIOS skills (markdown files defining slash commands) are stored in two locations: $HORIZON_BIN/skills/ (source of truth, committed to repo) and ~/.claude/skills/ (deployed copy, where Claude Code reads them).
Rationale: Claude Code reads skills exclusively from ~/.claude/skills/ — it cannot read them from an arbitrary path. The repo must track the canonical versions for community sharing and version history. Therefore, a deploy step is required. The bootstrap script handles initial deployment; subsequent skill updates require a manual re-copy or re-run of bootstrap.
Implications: The deployed copy is ephemeral and not committed. Editing skills directly in ~/.claude/skills/ creates invisible local changes that will be lost on the next deploy. All skill edits must happen at the source location. A future improvement would be a file-watch daemon or symlinks (where OS supports them) to eliminate the manual sync step.
2026-06-20 — Two-tier sounds directory structure
Decision: $HORIZON_BIN/sounds/ uses a two-tier structure. Generic sounds (usable by any harness) live at the root. Vendor-voiced sounds (containing AI product names or branded audio) live in subdirectories named <vendor>_event_sounds/.
Rationale: Generic sounds allow a hook to play audio without locking to a specific vendor. When a new harness is added, community contributors can add a vendor subdirectory with appropriate voiced audio without touching or conflicting with the generic tier. The naming convention (<vendor>_event_sounds/) is explicit and consistent.
Implications: Hooks that are shared across harnesses must use root-level generic sounds. Per-harness hooks may use the appropriate vendor subdirectory. No vendor sound should ever be placed at the root tier — it would break harness-agnosticism.
2026-06-20 — Global settings own hooks and statusLine; devroot settings own devroot permissions only
Decision: Event hooks (sounds on WorkComplete, InputNeeded, etc.) and the statusLine configuration live in ~/.claude/settings.json (the global Claude Code settings). The devroot-scoped $HORIZON_ROOT/.claude/settings.json owns only devroot-specific tool permissions.
Rationale: Hooks and the statusline are system-level behaviors that should fire in every Claude Code session, regardless of which directory Claude is launched from. If they were configured in the devroot settings, they would only be active when Claude is run from inside $HORIZON_ROOT. The global settings layer is the correct scope for OS-level behaviors.
Implications: When updating hooks or statusline config, edit ~/.claude/settings.json (the global file, not committed to the repo). The template at $HORIZON_SYSTEM/templates/claude_code/settings.json is the reference for what the global settings should contain. The devroot .claude/settings.json must never acquire hook or statusLine entries — if it does, those hooks will be double-fired when in devroot and silent elsewhere.
4. Files That Need Ongoing Synchronization
These pairs are the most likely to drift and require attention after any update.
4.1 Skills: $HORIZON_SYSTEM/skills_sbin/ (live via symlink)
Skills no longer require a copy/sync step. ~/.claude/skills/ is a symlink pointing directly to $HORIZON_SYSTEM/skills_sbin/. Changes to skill files in the repo are live on disk immediately.
The only action needed after adding or editing a skill: restart the Claude Code session so it re-reads ~/.claude/skills/.
Signs of a problem: a skill change in the repo has no effect after session restart. Verify that ~/.claude/skills/ is a symlink (not a real directory) and that its target is $HORIZON_SYSTEM/skills_sbin/. Re-run bootstrap if the symlink is missing.
4.2 Template-derived settings: $HORIZON_SYSTEM/templates/claude_code/settings.json → ~/.claude/settings.json
The template is the reference for what global settings should contain. When the template is updated (new hooks, new sounds, statusline changes), review whether your local ~/.claude/settings.json needs the same update.
The local file is NOT automatically updated — it is machine-local, with the single AIOS_EXEC_WRAPPER placeholder substituted to the home-relative aios-exec wrapper path. That substitution is AIOS-independent, so the file does not change on aios switch; only template content changes (new hooks/sounds) warrant a manual re-merge. Review the diff manually and merge changes.
Signs of drift: a new sound or hook that works on another machine does not work on yours. Compare your ~/.claude/settings.json against the template.
5. What Is NOT In the Repo (Local-Only)
Anyone who clones the Horizon AIOS repo will not have these. They must be created during setup (the bootstrap script handles most of them).
-
~/.claude/settings.json— Global Claude Code settings with machine-specific paths, hooks, and statusLine config. Created from the template at$HORIZON_SYSTEM/templates/claude_code/settings.json. -
~/.claude/CLAUDE.md— Machine-local stub containing a single@redirect to the repo's CLAUDE.md. Created during bootstrap (one line, never changes). -
~/.claude/skills/— A symlink created by bootstrap pointing to$HORIZON_SYSTEM/skills_sbin/. Not a deployed copy — skills are live on disk. The symlink itself is machine-local; the skills it points to are in the repo. -
$HORIZON_ROOT/.git/config— Local git configuration, includingcore.hooksPathwhich wires the pre-commit hook. Set by bootstrap viagit config core.hooksPath. -
$HORIZON_ROOT/handoffs/— Handoff documents from Claude Code sessions. Directory created by bootstrap; contents are session-local and not committed. -
Brain folders and their contents —
$HORIZON_ROOT/brains/<brain_name>/and everything inside. Brain configurations are per-machine and per-user; they are never committed to the shared repo. -
Any
*.local.jsonfiles — Machine-specific config overrides. Gitignored by convention.
2026-06-20 — Centralized logging taxonomy
Decision: (Path superseded — see Update below; canonical root is now $HORIZON_SYSTEM/logs/.) All AIOS operational logs are centralized under $HORIZON_ROOT/logs/ with subdirectories: bootstrap/, brain_provisioning/, agents_output/, hooks/, brains/. Log behavior is configurable via aios_local.conf (AIOS_LOG_MAX_DAYS, AIOS_LOG_MAX_SIZE_MB, AIOS_LOG_MAX_ROTATIONS). Weekly horizon_aios_maintain_logs.py in sbin/ handles pruning and rotation. The logs/ directory content is gitignored; a .gitkeep scaffold is tracked.
Rationale: Scattered log files make debugging cross-component issues difficult. A single root with a defined taxonomy makes logs discoverable and allows a single maintenance script to handle all log hygiene. Configurable retention keeps logs useful without unbounded disk growth. Gitignoring content but tracking .gitkeep ensures the directory structure is reproducible from a fresh clone without committing machine-specific log data.
Implications: Any new component that emits logs must write under $HORIZON_SYSTEM/logs/<subdirectory>/. The log directory is not created by the repo — bootstrap or the writing script must create it on first run (mkdir -p). horizon_aios_maintain_logs.py must be kept in sync with the taxonomy if new subdirectories are added.
Update (2026-06-21): The canonical log root moved from
$HORIZON_ROOT/logs/to$HORIZON_SYSTEM/logs/so the audit log lives inside the AIOS layer thathorizon_aios_harden.pylocks down (explicit brains-group Deny). All writers and$HORIZON_LOGSnow resolve to$HORIZON_SYSTEM/logs/. The subdirectory taxonomy is unchanged.
2026-06-20 — Token economy for context-loaded files
Decision: All files regularly loaded into Claude context (CLAUDE.md files, invariant docs, @ import chain) must be short, direct, and token-efficient. Verbose documentation belongs in $HORIZON_DOCS, not in context-loaded config.
Rationale: Every token in a context-loaded file costs tokens on every session that loads it. A verbose invariant document read hundreds of times per week accumulates significant token overhead. Documentation that provides background, rationale, and examples is valuable — but it belongs in files that are read by humans on demand, not in files that are injected into every AI context window automatically.
Implications: When adding to CLAUDE.md files or invariant documents, write for density: one sentence per concept, no illustrative examples, no repeated caveats. If elaboration is needed, put it in $HORIZON_DOCS and link to it. Review context-loaded files periodically and trim any content that is not load-bearing for AI behavior.
2026-06-23 — Sounds directory moved to $HORIZON_SYSTEM/sounds/ (supersedes 2026-06-20)
Supersedes: "2026-06-20 — Two-tier sounds directory structure" (which referred to $HORIZON_BIN/sounds/).
Decision: The sounds directory is $HORIZON_SYSTEM/sounds/ (env var $HORIZON_SOUNDS), not $HORIZON_BIN/sounds/. The two-tier structure (generic sounds at root, vendor-voiced sounds in <vendor>_event_sounds/ subdirectories) is unchanged.
Rationale: Sounds are AIOS-layer assets, not bin-layer executables. Placing them under $HORIZON_SYSTEM co-locates them with the rest of the AIOS layer, is consistent with how other non-executable assets (templates, harness_configs) are organized, and keeps $HORIZON_BIN reserved for executable scripts.
Implications: Any hook, script, or doc referencing $HORIZON_BIN/sounds/ must be updated to $HORIZON_SYSTEM/sounds/ (or $HORIZON_SOUNDS). The 2026-06-20 entry's two-tier structure and naming convention remain authoritative; only the root path changes.
2026-07-04 -- Two-lane sync supersedes fast-forward-only (refines 2026-06-22 distribution model)
Refines: "2026-06-22 -- Distribution & update model: framework vs. user-space." The framework-vs-user-space split, the own-remote backup, the leak guard, and the union-merge .gitignore rule are unchanged. Only the sync MECHANISM changes: horizon_aios_sync.py is no longer fast-forward-only, and the "fast-forward-only sync is a guardrail -- the sync refuses rather than silently clobbering" clause of the 2026-06-22 rationale no longer holds for official paths.
Decision: horizon_aios_sync.py runs a two-lane sync keyed on a fixed path partition (personal-owned = projects/, usrbin/, brains/; everything else = official-owned). The official lane (AIOS_OFFICIAL_REMOTE / AIOS_OFFICIAL_BRANCH) is authoritative and OVERWRITES official paths from upstream via a scoped hard-restore (git fetch, then git checkout <ref> -- <official paths>, then commit) -- it no longer refuses on a locally edited framework file; it reclaims it. The personal lane (AIOS_PERSONAL_REMOTE / AIOS_PERSONAL_BRANCH) is local-wins: default keeps local, SYNC_PERSONAL_FROM_REMOTE=yes opts in to a fast-forward-only advance, and --force-personal is the logged danger override that overwrites personal paths. SYNC_AIOS_FROM_REMOTE=no disables both lanes. The pre-two-lane AIOS_REPO_REMOTE / AIOS_REPO_BRANCH keys are retained as back-compat aliases mapped onto the official lane. Both lanes' automated commits pass git commit --no-verify to bypass the DCO commit-msg hook.
Rationale: The fast-forward-only guardrail assumed the operator would resolve any divergence by hand. In practice the system layer must track upstream unconditionally, so "upstream is authoritative and overwrites" is the correct default for official paths; the framework-vs-user-space contract already declares framework files are not a place to keep local edits, so reclaiming them is consistent, not surprising. Personal-owned paths are the operator's, hence local-wins with an explicit force override. The --no-verify carve-out exists because a machine sync commit is housekeeping, not a human contribution: the shipped DCO hook rejects any message lacking Signed-off-by, so without the bypass every automated sync commit would be rejected and abort the sync, leaving official paths overwritten-but-uncommitted (a dirty tree that re-overwrites each run).
Implications: New config keys AIOS_OFFICIAL_*, AIOS_PERSONAL_*, SYNC_PERSONAL_FROM_REMOTE; new --lane and --force-personal flags. Local edits to framework files are now silently reclaimed on the next sync -- the only durable customization seam remains the override layer. documentation/sync_setup.md is the authoritative operational reference; system/distribution_and_updates.md and getting_started/updating.md describe the model. The DCO bypass is scoped to the sync utility's own commits; all other commits remain subject to the hook. Follow-ups still open at time of writing: aios_local.conf.template and horizon_aios_setup_sync_schedule.py still emit only the deprecated AIOS_REPO_* names.