What changed
Aigis stops competing on detection-pattern count and becomes the tool that gets an AI agent through a company's security review. This is a direction change, not a feature drop-in — ROADMAP.md records the measurements that forced it.
- 2 new commands:
aigis profile(compose a role from six capabilities, and derive both enforcement layers from one file) andaigis settings(generate Claude Code's own permission rules from the Aigis policy). - 3 breaking removals: the
--policyflag, the[server]extra, and three subsystems that were never released. - 25,246 lines deleted: the dormant SaaS backend and dashboard, plus 15 dependencies that no module imported.
MAJOR bump because pip install pyaigis[server] and from aigis.aep import ... both stop working. Details and replacements below.
Version note: there is no usable 2.0.0. That number was uploaded to PyPI on 2026-04-11, during early development and before the project settled on 0.0.x and then 1.x, and PyPI refuses re-uploads even for a version whose files were deleted. 2.0.0 is yanked on PyPI — install 2.0.1.
Breaking changes, and how to migrate
aigis init --policy is removed
The four values (developer, reviewer, restricted, enterprise) all called the same code and then overwrote the policy's name, so --policy restricted restricted nothing. The only real behaviour behind the flag was enterprise initialising the signed audit-log key.
# before
aigis init --agent claude-code --policy enterprise
# after
aigis init --agent claude-code --signed-audit # same rules + the audit key
aigis profile build profiles/marketing.json # rules that actually differ per rolePassing --policy still parses: it prints where each part went and exits 1, rather than argparse's bare unrecognized arguments.
The [server] extra is removed
pip install pyaigis[server] no longer resolves. It declared 15 dependencies (uvicorn, sqlalchemy, alembic, asyncpg, PyJWT, passlib, redis, structlog, stripe, reportlab, openpyxl, ...) for the self-hosted SaaS backend, which is also removed. No module under aigis/ imported any of them.
aigis.aep, aigis.spec_lang, aigis.safety are removed
The Atomic Execution Pipeline, the policy DSL with its goal-conditioned FSM, and the Safety Specification Verifier. Each met all three conditions: imported by no other module, exported from no public API (aigis/__init__.py never referenced them), and never released — no tag exists for v1.3.0 through v1.5.0, the versions credited with introducing them.
Guard.authorize_tool() stays, along with capabilities/, supply_chain/ and cross_session/.
aigis profile — compose a role from capabilities
A company rolling Claude Code out has to answer "who may do what" per department. Shipping a marketing profile would ship our assumptions: marketing means one thing where marketers query the warehouse and another where they write copy. What is stable across companies is what a person needs to be able to do, so a role is a combination of six axes.
| Axis | Values |
|---|---|
web |
none · read |
files |
none · read · workspace · unrestricted |
shell |
none · unrestricted |
git |
none · local · push |
packages |
none · unrestricted |
mcp |
none · approved · unrestricted |
$ aigis profile build profiles/marketing.json
Profile: Marketing
web read Can fetch web pages and search the web
files workspace Can read and write inside the working directory
shell none Cannot run shell commands
git none Cannot use git
packages none Cannot install dependencies
mcp approved Can use MCP servers the administrator listed
Wrote aigis-policy.yaml (30 rules)
Wrote .claude/settings.json (24 permission rules)
One file produces both layers, so they cannot drift apart. Three starter profiles ship in profiles/, labelled as starting points rather than answers.
Three properties are deliberate, each pinned by a test:
- An axis a profile does not name gets the most restrictive value. Silence must not grant capability, so a half-written profile fails closed.
- The capability layer never emits
review. A prompt only protects someone who can judge it; in practice a non-engineer either approves everything, which defeats it, or refuses everything, which stops the work. Judgement happens once, at profile-design time, for the whole group. This is also whyshellhas noallowlistandpackagesnoapprovedvalue. - The baseline cannot be weakened (
.env,~/.ssh, credentials,rm -rf, pipe-to-shell), whatever the combination.
The third property caught a real hole during implementation. Capability rules are evaluated before the baseline, so a git: unrestricted value would have emitted an allow for git push* sitting in front of the baseline's own *--force* deny. git therefore has three values, force-push stays denied for everyone, and a test asserts git is the only axis permitted to emit an allow at all.
aigis settings — generate Claude Code's own permission rules
Claude Code evaluates its deny and ask rules regardless of what a PreToolUse hook returns, which makes its permission list the outer gate and the Aigis hook an inner one. Aigis used to generate only the inner layer and leave the outer to be written by hand — which is where mistakes spread. The settings circulating in write-ups have already drifted: disableBypassPermissionsMode, widely quoted as the official key, does not appear in the current documentation. The documented key is disableAutoMode.
$ aigis settings --managed
{
"permissions": {
"deny": ["Bash(rm -rf *)", "Edit(.env*)", "Write(.env*)"],
"ask": ["Bash(sudo *)", "Read(*secrets*)"]
},
"disableAutoMode": "disable"
}
Converted 6 of 16 policy rule(s) into 10 permission rule(s).
10 policy rule(s) could NOT be expressed as Claude Code permissions...
[ssh_key_protection] file:* *.ssh/* -> deny
why: target spans a path separator; Aigis matches it with fnmatch (where `*`
crosses directories) while Claude Code uses gitignore syntax
fix: Write the rule by hand, e.g. Read(**/.ssh/**).
The conversion refuses to approximate. Aigis matches with fnmatch globs; Claude Code anchors Bash() specifiers at the start of the command and uses gitignore syntax for paths. Rules that cannot be translated exactly are excluded and reported with a reason and a hand-written alternative — never emitted as something that looks equivalent but is looser. Silently loosening a security rule is the one failure that matters here.
--managed also prints the three OS install paths for managed-settings.json, where a permission rule cannot be overridden by any other settings level, including command line arguments.
Know before you deploy: the policy is a deny-list
The shipped rules enumerate known-dangerous operations (rm -rf, mkfs, dd, .env writes, ~/.ssh access, piping a download into a shell) and let everything else proceed, because an agent that cannot run ls is not usable. So an operation no rule covers will run.
That is a deliberate design, not an oversight, but it is the first thing a security reviewer will ask about — so the generated trust pack now states it outright, explains what it implies, and gives the recipe for a fail-closed posture (default_decision: deny plus explicit allow rules) together with an honest note that enumerating those rules is real work.
The same section documents the audit log's key-management boundary: the HMAC key lives on the same machine as the agent, so the signed log detects tampering by anyone except the log's own author. Where that matters, pair it with SIEM forwarding — events mirrored off-box are beyond the developer's reach.
Also in this release
release_preflight.shgained a PyPI availability check. The gate used to verify tag collisions, orphan commits and master-tip, but never whether the version number was still available — which is how the 2.0.0 attempt got as far as a pushed tag.- Scorecard now uses
GITHUB_TOKEN, and the repository moved from classic Branch Protection to Repository Rules, which Scorecard reads with the default token. - The marketing site (
site/) was removed.aigis.dev301s to an unrelated project, and every path except/redirected to a malformed host, so the Next.js app was never reachable at the domain it declared.
Tests: 1781 pass · 0 fail · 0 skipped
Full Changelog: v1.2.0...v2.0.1
