Claude Code harness and alternative to context compaction: at 80% context it writes a verified capsule, runs /clear and resumes with persistent memory in git.
install.sh is a local installer. It activates the downloaded checkout in place or copies the framework into a target selected by the user. This document describes the current behavior, including file mutations, network access, trust boundaries, and rollback paths.
less install.sh
bash install.sh --help
bash install.sh --target /path/to/project --dry-run
The dry run reports the source, target, mode, copied directories, configuration changes, and whether optional dependencies would be installed. It does not create the target directory or modify files.
The installer writes only inside the resolved target directory.
For an external target it may:
system/, vault/, skills/, hooks/, daemons/, scripts/, docs/, memory/, and evals/ directories.CLAUDE.md..claude/rules/, .claude/skills/, and .claude/agents/..claude/settings.json..aigent/state.json and .aigent/backups/..gitignore.daemons/semantic-search/.The installer does not:
sudo.PATH, global Git configuration, or files outside the target.hooks/, daemons/, scripts/, launcher/, .claude/skills/, .claude/agents/, .claude/rules/, and skill-index.json quarantine a differing pre-existing file instead of silently keeping it (see below), unless that path is declared operator-owned (see “Operator-owned paths”)..claude/settings.json.Files copied from framework directories use no-clobber behavior. Existing destination files remain untouched. This protects user customizations but also means rerunning the installer is not a blind upgrade mechanism for modified framework files.
hooks/, daemons/, scripts/, launcher/, .claude/skills/<name>/, .claude/agents/*.md, .claude/rules/, and .claude/skill-index.json are all treated differently from every other framework tree, because their content becomes trusted the next time Claude Code touches it: hooks and daemons run on a lifecycle event, scripts carries the baseline manifest doctor.sh --attest reads, launcher carries the platform installers the launcher-wiring step runs, skills and agents are read and dispatched as slash commands/subagents, rules are read as agent instructions every session, and skill-index.json drives which skill gets auto-invoked. If a file already exists at a path the installer would otherwise place trusted content at, and its content differs from the framework’s version, the installer quarantines the existing file (moves it to .aigent/quarantine/<path>.<timestamp>) and installs the trusted framework copy instead, printing a [quarantine] line naming both paths. A pre-existing file whose content is byte-identical to the framework’s is left alone (no-op, same as any rerun).
This exists so that installing into an existing project directory – one that might already contain a planted file at, say, hooks/security-scan.sh or .claude/skills/open/SKILL.md – cannot silently leave that planted file in place to be wired up as trusted content. If you intentionally maintain your own customizations at any of these paths, pass --trust-existing to keep them instead of quarantining:
bash install.sh --target /path/to/project --trust-existing
--trust-existing is all-or-nothing: it keeps every pre-existing trusted-tree file, which also means a genuine core fix shipped in a newer version is never applied. Prefer declaring the specific paths you own, below, so everything else still upgrades.
Create <target>/.aigent/operator-owned.json to name the paths you maintain yourself:
{
"schema": "OperatorOwnedPaths/v1",
"paths": [
".claude/skills/*/SKILL.md",
".claude/agents/my-reviewer.md",
".claude/skill-index.json"
]
}
Entries are paths relative to the install target. A single * matches within one path segment and never across a /; ** is not accepted. Semantics:
[operator-owned] keep <path>.The installer refuses to run, with a named error and without writing anything into the target, if the file cannot be parsed, if an entry is absolute or contains a .. or empty segment, if an entry is declared twice, if an entry resolves through a symlink inside the target, if it uses **, if it names a path the installer rewrites through another mechanism (CLAUDE.md, .gitignore, .claude/settings.json, .claude/settings.json.template), or if it names a file pinned as core-required in scripts/fleet-baseline-manifest.json. An entry under no installer-managed tree is a warning rather than a refusal: it is inert, because the installer never writes there in the first place.
Reading the declaration requires python3, the same runtime scripts/doctor.sh --attest already needs to read it. Installs with no declaration file are unaffected.
scripts/doctor.sh --attest reads the same file so its verdict stays honest about ownership. A path pinned in required_files that differs from the framework copy and is declared operator-owned is reported as OPERATOR-OWNED <path> (declared; hash differs from core) and attests DEGRADED. It is not COMPLIANT, because the installer refuses to create that declaration, so finding one means the file was declared by hand and core-required content has drifted. It is not NONCOMPLIANT on that path alone either, because the operator did claim ownership of it. The summary states the split as ownership N core-owned, M operator-owned.
An undeclared pinned path that differs is still NONCOMPLIANT. A declared path outside required_files is not in the measured population and does not affect the verdict. A declared path that is missing is still a failure, because the installer would have placed core there. If the declaration file is present but unreadable the verdict is UNKNOWN: the instrument cannot tell core-owned from operator-owned, which is a failure to measure rather than a measurement that the tree is broken.
If you automate on this, parse the terminal string, not the exit code. DEGRADED exits 0, exactly like COMPLIANT; only NONCOMPLIANT exits 1 and UNKNOWN exits 2. A gate written as bash scripts/doctor.sh --attest && echo ok therefore treats declared drift on a core-required path as success. Match the ATTEST: <terminal> line if you need to catch it. The summary line ownership N core-owned, M operator-owned, K missing accounts for the whole measured population, so those three numbers always add up to the population count printed above them.
The declaration lives under .aigent/, which the installer’s managed .gitignore block already excludes, so it stays a local per-install statement of ownership and is never committed out of the target by accident.
.claude/rules/post-compact-critical.md keeps its own unconditional preservation and needs no declaration; declaring it explicitly is accepted and behaves identically.
.claude/settings.json.template is exempt from this treatment for a different reason: it is unconditionally regenerated from the framework’s copy on every run (never gated on “already exists”), so it can never silently keep a planted or stale template in the first place.
scripts/doctor.sh reads every hook command in .claude/settings.json and checks where the script it runs resolves. A hook that runs a script inside another install’s tree silently stops working the moment that other tree moves or a file there is renamed, so the doctor treats such a reference as a failure.
daemons/ or hooks/ path pinned in scripts/fleet-baseline-manifest.json) must resolve to this install’s own copy. Pointing a core hook at any other tree fails, even a declared one: core runs core.A hook that is an operator extension may resolve inside this install or inside a declared shared extension root. Declare those roots in <target>/.aigent/shared-extension-roots.json as a JSON array of absolute directory paths:
["C:/shared/aigent-extensions"]
A script resolving under a declared root passes. Anything outside this install and outside every declared root fails with hook references a path outside this install. A present-but-malformed declaration file is reported, not silently ignored.
The default install declares no shared roots, and every core hook resolves under __AIGENT_ROOT__, so a stock install passes with no declaration.
Every write inside the target – creating a directory, copying a framework file, writing CLAUDE.md/settings.json/.gitignore/.aigent/state.json, and the .aigent/backups//.aigent/quarantine/ copies those two make of a pre-existing file before overwriting it – is checked first: if any path component from the target root down to (and including) the destination is already a symlink, the write is refused rather than followed. Without this, a pre-seeded symlink such as a file named CLAUDE.md that actually points at ~/.bashrc would let a write we believe lands on $TARGET/CLAUDE.md land wherever the link points instead, since both cp and shell redirection follow symlinks by default. Single critical writes (CLAUDE.md, .gitignore, .claude/settings.json, and their .aigent/backups/ copies) abort the whole install with an actionable error; copies of many files (framework trees, skills, agents, and their .aigent/quarantine/ copies) skip just the affected file with a [skip] warning and continue.
External installations use these markers:
<!-- aigent-os:start -->
<!-- aigent-os:end -->
On rerun, only the marked block is replaced. The previous CLAUDE.md is copied into .aigent/backups/ first. Older installations that contain an unmarked appended copy cannot be safely identified automatically and may require one manual cleanup.
A valid existing .claude/settings.json is backed up and deep-merged. Existing user values are preserved except the managed AIGENT_ROOT and AIGENT_VAULT entries, which are refreshed to the target path. Hook arrays are extended without adding byte-identical duplicates.
If the existing file is invalid JSON, it is left untouched and the rendered aigent settings are saved as .claude/settings.aigent.json.
Only the marked aigent generated-state block is refreshed. Existing ignore rules remain.
The core copy and configuration steps use local files only.
Unless --no-deps is supplied, the installer may run:
npm ci --silent --ignore-scripts
or:
npm install --silent --ignore-scripts
inside daemons/semantic-search/ and daemons/transport-deps/ (two independent optional dependency roots: the embeddings feature and the auto-clear transport’s node-pty, respectively). npm may contact configured registries and resolve each dependency lock. --ignore-scripts disables lifecycle scripts (preinstall/install/postinstall) for each package and every transitive dependency, so a compromised transitive dependency – or a package.json under daemons/ that already existed in the target before this install ran – cannot execute arbitrary code as a side effect of dependency resolution. Neither bundled package has lifecycle scripts of its own, so this changes nothing for a normal install. Review each package.json and lock file before enabling this step in a high-trust environment regardless.
Skip it with:
bash install.sh --no-deps
Running the installer trusts:
PATH.After installation, Claude Code hooks run with the user’s operating-system permissions. Review .claude/settings.json and every command hook before using the system with sensitive files.
Prefer a tagged release or pinned commit:
git rev-parse HEAD
git status --short
Tagged releases are not a substitute for signed provenance; consult SECURITY.md for the current signing status.
INSTALL.md’s one-line bootstrap (curl | sh / irm | iex) always fetches whatever tools.theaigent.xyz currently serves and executes it immediately – convenient, but a compromise of that domain, its DNS, or a MITM position between you and it would run arbitrary code before any check in the fetched script itself has a chance to fire.
The checksum-pinned variant instead fetches both the bootstrap script AND its checksum from raw.githubusercontent.com at a commit you pick – a different domain and TLS chain than tools.theaigent.xyz – and verifies the download before running anything.
Pick a commit that contains scripts/web-install.sh.sha256: the latest commit from the commit history, or a tagged release from the tags page – older tags predate this file, so confirm the tag you pick actually includes it (git ls-tree -r <tag> --name-only | grep web-install.sh.sha256 against a local clone, or browse the tag on GitHub).
COMMIT=<paste-a-commit-sha-here>
curl -fsSL "https://raw.githubusercontent.com/wrg32786/aigent-os/$COMMIT/scripts/web-install.sh" -o /tmp/aigent-os-install.sh
curl -fsSL "https://raw.githubusercontent.com/wrg32786/aigent-os/$COMMIT/scripts/web-install.sh.sha256" -o /tmp/aigent-os-install.sh.sha256
(cd /tmp && sha256sum -c aigent-os-install.sh.sha256) && sh /tmp/aigent-os-install.sh
$commit = "<paste-a-commit-sha-here>"
Invoke-WebRequest "https://raw.githubusercontent.com/wrg32786/aigent-os/$commit/scripts/web-install.ps1" -OutFile "$env:TEMP\aigent-os-install.ps1"
Invoke-WebRequest "https://raw.githubusercontent.com/wrg32786/aigent-os/$commit/scripts/web-install.ps1.sha256" -OutFile "$env:TEMP\aigent-os-install.ps1.sha256"
$expected = (Get-Content "$env:TEMP\aigent-os-install.ps1.sha256").Split(' ')[0]
$actual = (Get-FileHash "$env:TEMP\aigent-os-install.ps1" -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected) { throw "checksum mismatch -- do not run this script" }
& "$env:TEMP\aigent-os-install.ps1"
What this does and does not protect against:
tools.theaigent.xyz, its DNS, or a MITM position targeting that CDN specifically – the executed bytes are verified against a checksum recorded in this repo’s git history at a commit you chose, over a completely separate domain and trust chain.SECURITY.md); until then, pinning to a commit you can read the diff of is the strongest guarantee available.scripts/web-install.sh.sha256 and scripts/web-install.ps1.sha256 are regenerated with bash scripts/gen-web-install-checksums.sh whenever the corresponding script changes; tests/test-web-install.sh fails CI if they ever drift out of sync with the scripts they checksum.
Search the installer for unexpected capabilities:
grep -nE 'curl|wget|sudo|eval|exec|source |npm |pip |ssh |scp ' install.sh
Expected results should correspond only to documented optional dependency handling or explanatory text.
The installer does not ask for or transmit credentials. Hook activity capture stores metadata only and deliberately omits raw Bash commands, MCP queries, arbitrary tool input, and file contents. The tracker also redacts common credential formats before writing.
This is defense in depth, not a guarantee that every secret format is recognizable. Keep secrets out of command lines when possible, restrict vault permissions, use disk encryption where appropriate, and review generated daily notes before sharing them.
Temporary installer files are removed through an exit trap. Backups of managed configuration are kept under .aigent/backups/.
To inspect a failed installation:
bash scripts/doctor.sh /path/to/install
Do not delete backups until the installation and hooks have been exercised in a fresh Claude Code session.
Report installer path traversal, command injection, secret logging, unsafe overwrite, dependency confusion, or writes outside the target through the private process in ../SECURITY.md. Include the tested commit, operating system, exact command, and minimal reproduction.