Skip to content

Why I turned Agent Engineering Rules into an install-once CLI

5 min read

AER v3.0.1 replaces a GitHub-tag npm command with an install-once CLI while keeping every managed file inside the target repository.

AER v3.0.0 had a working CLI, but its primary installation command made every user confront the repository and npm transport I had chosen to distribute it:

npm exec --yes --package=github:aaarslan/agent-engineering-rules#v3.0.0 -- \
  aer init --host claude --target . --dry-run

That command was reproducible, but it was not a good interface. It exposed a GitHub tag, npm's one-shot execution syntax, a package source, the executable name, and a target path before the user had even previewed one repository. The plumbing had become part of the product.

The smaller interface

v3.0.1 makes the ordinary path install once, then use the command directly:

npm install --global @aaarslan/aer

aer init --host claude --dry-run
aer init --host claude
aer doctor

The package is public, the executable is aer, and the normal command no longer asks users to understand how I packaged the repository. Claude Code and OpenAI Codex are the only supported hosts. The package has zero runtime dependencies and requires Node.js 24 or newer.

Global executable, local ownership

The word global is easy to misread here. npm installs the aer executable into its global prefix so the shell can find it. AER does not use that location for agent configuration. Generated rules, skills, ownership state, and managed markers stay inside the repository selected by the user.

That boundary is deliberate. v3.0.1 does not modify user homes, global Claude or Codex settings, unrelated repositories, consumer CI, Git hooks, services, daemons, or accounts. It adds no telemetry, install script, native binary, or automatic publishing workflow. The project owner still decides which hosts to install, which repository checks to enforce, and whether to commit the generated files.

Initialization is also intentionally greenfield. If a target already contains an unowned file at a path AER would manage, even if its bytes happen to match, aer init refuses the collision. The CLI will not infer ownership from resemblance. That is why I did not use this release-content change to overwrite the older manually copied rules in my portfolio repository.

Why this is v3.0.1

The v3.0.0 GitHub tag and release already identified a specific source and distribution state. Publishing the scoped package changed package metadata, public distribution, documentation, version reporting, and release validation. Moving the old tag would have made the same version name point at different source over time, so I left the immutable v3.0.0 history alone and recorded the distribution patch as v3.0.1.

That patch boundary matters. v3.0.1 did not change the rule corpus, profiles, contexts, Claude or Codex payloads, supported-host boundary, ownership model, or project-state schema. It makes no new claim that the rules improve model behavior. This release proves a distribution path and its safeguards, not efficacy.

Two upgrades that should not be one command

Installing a newer CLI and applying that CLI to a repository are separate operations. I kept them separate because they have different scopes and different evidence. The npm command changes the program on the machine; aer update changes only content whose project-local ledger proves AER owns.

# Upgrade the installed CLI
npm install --global @aaarslan/aer@latest

# Preview, then update one managed repository
aer update --dry-run
aer update

Running aer update does not fetch or install a newer CLI. It updates a managed repository using the version already installed. Upgrading the CLI does not silently walk existing repositories and modify them. The user performs both mutations explicitly.

The package is an allowlist

The npm package is not a tarball of the whole development repository. package.json names the files allowed into the artifact: the CLI, installation logic required at runtime, generated Claude and Codex distributions, license, and user documentation. Research inputs, development tests, repository configuration, and internal metadata do not ride along accidentally.

The published v3.0.1 package contains 95 files. It is 72,560 bytes compressed and 302,137 bytes unpacked. The allowlist keeps the artifact reviewable; the isolated packed-install test checks that the allowlist is also complete. A package can look tidy and still be broken if it omits one file the executable expects after installation.

The packed test creates isolated npm prefixes and caches, installs the actual tarball, invokes the installed shim, initializes a clean Git repository, runs doctor, updates it, and uninstalls it. Linux and Windows release jobs passed. The published package then passed a fresh isolated Windows installation and clean-project lifecycle smoke test.

The release failure that was not a package defect

One failure during release was useful precisely because it was deterministic. npm publish --dry-run exported npm_config_dry_run=true, then ran prepublishOnly, which runs the full release:check. Inside that gate, the packed-install smoke test used a nested offline npm exec --package=. to exercise the local package.

The nested npm process inherited the outer dry-run flag. npm simulated the temporary install instead of materializing its _npx directory, and the smoke test then failed when it could not find the temporary package.json. The complete release gate passed when run normally. The real publish reran prepublishOnly without the outer dry-run flag and passed before publishing.

I did not classify that as transient, because repeating the same lifecycle environment repeats the same failure. It was a lifecycle-environment lesson: nested package-manager commands inherit npm configuration unless the test isolates the environment it intends to measure. The immutable v3.0.1 tag was never moved or recreated.

The complete command lifecycle

These are the ordinary commands I now expect a user to need:

# Install the CLI
npm install --global @aaarslan/aer

# Initialize a repository
aer init --host claude --dry-run
aer init --host claude

# Inspect ownership and drift
aer doctor

# Upgrade the CLI
npm install --global @aaarslan/aer@latest

# Update a managed repository with the installed CLI
aer update --dry-run
aer update

# Remove managed content, then remove the CLI
aer uninstall --dry-run
aer uninstall
npm uninstall --global @aaarslan/aer

For a reproducible one-shot run, the exact npm version remains available without a global install:

npm exec --yes --package=@aaarslan/aer@3.0.1 -- \
  aer init --host claude --dry-run

What remains unproven

This release closes a usability and release-engineering gap. It does not establish that v3 rules improve Claude or Codex output, and it does not extend support to other hosts. Node.js 24 is a real minimum. Greenfield collision refusal means repositories with older hand-copied rules need a reviewed adoption plan rather than a force flag. The CLI also does not upgrade itself, enable the optional advisory hook, or create enforcement the repository does not already own.

That narrower result is still the one I wanted from v3.0.1: install the CLI once; keep all agent configuration local to each repository; make every later mutation explicit and inspectable.

  • agents
  • developer tooling
  • release engineering
  • npm

Contact

Let's build something that ships.

Open to conversations about senior and staff frontend work, AI application engineering, and hard product problems. The fastest route is email.

© 2026 Abdallah Arslan · Atlanta, GA · Remote

React 19 · TypeScript · Tailwind · WebGL · d dark mode · ⌘K commands