Skip to content
ampersantePublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

actx

Context compression for AI-agent shell commands.

actx is a small, dependency-free CLI that compresses the output of supported shell commands before it reaches an AI agent — fewer tokens, shorter context windows, cheaper and more focused agent sessions.

$ git status        →   $ actx git status

Python 3.14 Stdlib only Tests


Why

AI agents run git status, git diff, grep, find, ls, tests, and installers, then read their full output. Most of that output is noise: progress bars, repeated lines, boilerplate. actx sits between the agent and the shell, rewrites supported commands through itself, and returns a compact, structured summary of status and listing output (content such as git diff, grep or cat stays byte-for-byte) — while keeping the original exit code and a recoverable copy. Secret values are masked on every output path.

Demo

Raw git status:

On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   src/main.py
	modified:   src/utils.py
	modified:   src/models/user.py

actx git status:

* main
M (3):
  src/main.py
  src/utils.py
  src/models/user.py

actx git diff, actx grep and other content commands print the command's output byte-for-byte except masked secret values:

+API_TOKEN=‹masked›
+DATABASE_URL=postgres://app:‹masked›@db:5432/app

Features

  • L7 Security Gatekeeper (actx hook) — deterministic PreToolUse guard (<0.02ms latency) that intercepts dangerous commands before execution. Blocks credential exposure (.env, ~/.ssh/id_rsa, AWS/GCP keys, Keychain), network exfiltration (curl -d @.env, nc/socat), pipe-to-interpreter obfuscation, OS destruction (rm -rf /), and insecure package registries. Requests human confirmation (ask) on destructive git operations (git push --force, git reset --hard).
  • Auto-rewrite (observational + narrow mutators) — observational CLI (git RO, ls/grep/find/wc/…, test runners, linters without write flags, docker/kubectl/gh RO, …) plus a narrow mutator allow-list (git add|commit|push|pull|fetch; package installs left it and always ask for confirmation). Safety is metachar/exec/fail-open/write-flag rejects (--fix, ruff format, …), not “read-only only”. No lexer; no python3/aws auto-rewrite.
  • Exact exit codes — the original command's exit code is preserved, including git status returning 128 outside a repo and grep returning 1 for no matches.
  • Stderr never hidden — a compacted command (tests, linters, installers) prints its stderr after the summary, secret values masked: whole on failure; on success whole up to 20 lines, longer stderr keeps its warning/error/URL lines plus one [actx] N stderr lines hidden; full output: <tee path> line (actx run --failures stays silent on a green run).
  • Lossless filenames — git status, ls, and find compress the format, not the names.
  • Tee for recovery — truncated or failed output is saved to ~/.local/share/actx/tee as JSON (secret values masked); a truncated result is always saved and its tee path printed.
  • Fail-open everywhere — hooks, adapters, and filters never throw into the agent on an error (the OpenCode plugin's deliberate throw on a security-gate deny is OpenCode's only way to block a command); on any error they pass the command through unchanged and the output through with secret values masked (if masking itself fails, lines that may carry a secret are withheld and a marker line says how many).
  • Escape hatch — ACTX_BYPASS=1 or a bypass_commands entry runs the matching command unfiltered (byte-for-byte except masked secret values), preserving the exit code.
  • Stdlib only — Python 3.14 standard library. No pip, no brew, no network, no telemetry.
  • Multi-agent — deterministic hooks and plugins for Claude Code, Codex, Gemini/Antigravity, GitHub Copilot CLI, OpenCode, Devin CLI, and pi; rule-based instructions for Grok, Cline, Windsurf, and Cursor.

Installation

Requirements: Python 3.14 (macOS or Linux). No third-party packages.

From source

chmod +x actx
# Put `actx` on PATH, e.g. symlink into a bin directory:
ln -s "$(pwd)/actx" ~/.local/bin/actx

Then install the adapters for the agents you use:

actx init              # auto-detect installed agents
actx init --agent all  # install for every supported agent
actx init --agent claude
actx init --agent codex
actx init --agent opencode
actx init --agent devin
actx init --agent pi

After installation, restart the agent.

Via curl (release tarball)

curl -fsSL https://github.com/ampersante/actx/archive/refs/tags/v2.14.0.tar.gz | tar xz
cd actx-2.14.0
bash install.sh

Keep the extracted directory — install.sh symlinks the actx binary from it.

Via Homebrew

brew tap ampersante/actx
brew trust ampersante/actx
brew install actx

brew installs Python 3.14 automatically via the formula dependency.

Uninstall

Step 1 first — while actx is still installed at the same path:

actx init --agent all --uninstall
actx init --show    # every agent except Cursor should read "not installed"

Cursor always shows manual (cursor) — that is fine, it was never written to disk. You will also see a Cursor "nothing to uninstall" line on stderr; that is expected. If you pasted the section into Cursor UI, remove it there by hand. Restart the agent(s) before the next step.

Then remove the program:

rm ~/.local/bin/actx    # symlink install
brew uninstall actx     # instead, if you installed via Homebrew

If you installed by adding the actx directory to PATH, remove that line from your shell config instead. If you installed via curl, also delete the extracted directory.

Then remove user data (these are actx's own directories):

rm -rf ~/.config/actx ~/.local/share/actx

If you cloned actx, delete that folder too.

If you moved actx after installing, move it back to its original path, run Step 1, then delete it; otherwise remove the old hooks from the agent config by hand.

Usage

actx [--raw] [-v|-vv|-vvv] <command> [args...]
actx run <cmd...>
actx gain [--graph|--history|--daily|--breakdown] [--format json]
actx discover
actx session
actx tracking [on|off|status|clear]
actx rewrite "<command>"      # pure rewriter, no security gate
actx hook [--agent codex|opencode|devin|pi] [--payload <json>]
actx init [--agent <name>|all] [--show] [--uninstall]
actx --version
actx --help
Command Behavior
actx git status Compact grouped status: * branch, M (n):, ?? (n):
actx git diff [args] Output byte-for-byte except masked secret values
actx git log [args] git log --oneline output
actx ls [path] Directories first, then files, grouped by path
actx grep [args] <pattern> [path...] Output byte-for-byte except masked secret values
actx find [args] Groups paths by directory
actx wc/head/tail/sort/uniq [args] Read-only; output byte-for-byte except masked secret values
actx read <file> [--level minimal] Strips full-line comments for known extensions
actx run <cmd...> Generic wrapper: lossless collapse, explicit truncation marker (a truncation always tees and prints the tee path), tee on failure
actx run --digest <cmd...> Head+tail with skipped-line count; use for large python3 output
actx gain Savings summary: calls, bytes/≈tokens, %, top categories and strategies
actx gain --breakdown Savings composition by compression strategy
actx discover Candidates for new filters (frequent passthrough commands)
actx session Adoption across sessions
actx insights [--days N] [--top N] [--verbose-commands] [--json] Orchestration analytics: repeats, failures, passthrough, suggestions, verbose-command advisor, wave-head adoption
actx tracking on|off|status|clear Enable/disable/inspect/clear local analytics
actx git add/commit/push/pull/fetch Narrow mutators (auto-rewritable); success prints a tiny confirmation
actx git branch [RO flags] Branch list / show-current (RO flags only)
actx --raw <command> Bypass filtering; output byte-for-byte except masked secret values
ACTX_BYPASS=1 actx <command> Run the command unfiltered (no filter, no tee; secret values masked)

ACTX_BYPASS=1 disables filtering for that call; adding a command's first token to bypass_commands in the config does the same for every call of that command. Everywhere, -v/-vv/-vvv (before the subcommand) control debug output to stderr.

Examples

actx git status
actx git diff --stat       # byte-for-byte; --stat is git's own compact form
actx ls src
actx grep "TODO" src/
actx find . -name "*.py"
actx read main.py --level minimal
actx run pytest tests/
actx run --digest python3 parse_data.py   # large parsing output: head+tail
actx insights --json        # orchestration analytics as JSON
actx insights --verbose-commands   # top verbose passthrough heads + suggested compact flags
actx --raw git status      # unfiltered: byte-for-byte except masked secret values

python3 is never auto-rewritten (arbitrary code, not provably read-only). For large python3 parsing output, use actx run --digest python3 ...; the script itself should print compact structured output where possible.

How it works

agent shell call
      │
      ▼
agent adapter (hook / plugin / rules)
      │
      ▼
actx hook [--agent X]   →  deny | ask | allow + "actx <command>" | nothing
      │
      ▼
actx CLI: parse → route → execute (exec-array) → filter → print → tee
      │
      ▼
compact output + original exit code

One rewriter is the single source of truth for every adapter. It rewrites simple allow-listed commands (no compounds/lexer) and returns the original command string verbatim with an actx prefix — never rebuilt from tokens.

Agent integration

Tier-1 hooks (Claude/Codex; Devin and pi do not deliver it) append a compact-flag hint to additionalContext on allow+rewrite verdicts for known verbose forms (e.g. a bare git log gets the -n advice); deny/ask verdicts and the Antigravity schema carry no hints, and a clean command without a rewrite still returns strict no-op. Tier-2 instruction files get the same conventions as a "Prefer compact flags" block (actx init regenerates it; a broken render degrades to the section without the block). Known limitations on the Antigravity (gemini) schema: no additionalContext field (agents rely on the Tier-2 block only), and no true "defer" verdict — decision is required, so commands with no actx verdict and no rewrite return ask (the documented defer primitive; it prompts but honors the "Always Allow" cache). Under --dangerously-skip-permissions Antigravity still honours a hook deny, but it auto-approves ask and force_ask, so a gate confirmation (e.g. git push --force) runs without a prompt.

Agent Mechanism Install Auto-rewrite
Claude Code PreToolUse JSON hook actx init --agent claude ✅ deterministic
Codex PreToolUse JSON hook actx init --agent codex ✅ deterministic¹
Gemini / Antigravity PreToolUse JSON hook actx init --agent gemini ✅ deterministic
GitHub Copilot CLI PreToolUse JSON hook actx init --agent copilot ✅ deterministic
OpenCode ≥ 2.0.0 TypeScript plugin (v2) actx init --agent opencode ✅ deterministic²
Devin CLI PreToolUse JSON hook (exec tool) actx init --agent devin ✅ deterministic²
pi TypeScript extension actx init --agent pi ✅ deterministic²
Grok Build Rules instruction actx init --agent grok soft (~70–85%)
Cline / Roo Rules file actx init --agent cline soft
Windsurf Rules file actx init --agent windsurf soft
Cursor Printed for manual UI insert actx init --agent cursor manual

¹ Codex requires the user to trust the hook once via /hooks. With actx ≥ 2.13.1, the security gate runs first, then eligible Git mutators (git add|commit|push|pull|fetch) are left unrewritten so existing native permission rules match the original command. The hook returns no new allow for these commands. Trusted hook definitions are unchanged.

² These adapters call actx hook --agent <name> and require actx ≥ 2.13.0. Per harness:

  • OpenCode checks its own permission rules against the command after the plugin has rewritten it. So the git mutators (git add|commit|push|pull|fetch) are left unrewritten, and your rules such as shell:git push* still match them. OpenCode has no confirmation channel for plugins, so a gate ask (e.g. git push --force) becomes a deny with the reason actx security gate (opencode: no confirmation channel) …. Not covered: user-typed !cmd, the execute tool (code-mode bridge for catalog/MCP tools), MCP tools. The agentic-kit apply writes the same plugin file; until the kit drops its template, re-run actx init after a kit apply.
  • Devin also checks its rules after the hook. Git mutators are allowed without being rewritten, so a matching deny rule of yours still wins. A gate ask prompts; devin -p rejects it. Mutator forms actx does not rewrite (git -C dir push) get Devin's default prompt, which -p rejects. ~/.config/devin/config.json (or $XDG_CONFIG_HOME/devin/config.json) is merged in place and keeps mode 0600; if it contains comments or an unexpected shape, actx init leaves it untouched, exits 1 and prints the entry to add by hand.
  • pi never asks the human: a gate ask becomes a deny with the reason actx security gate (pi: no confirmation channel) …, so the agent sees why and continues. The extension goes to $PI_CODING_AGENT_DIR/extensions/actx.ts (default ~/.pi/agent/extensions/actx.ts). Not covered: user-typed !cmd.
  • In OpenCode and Devin, a rule of yours on an observational command (e.g. git log*) no longer matches once actx rewrites it to actx git log ….
  • Devin with the Tier-2 rules file only (no hook): devin -p rejects a rewritten actx git status as "requires confirmation"; add an allow rule for actx *. Since actx 2.13.0 Devin is wired as a Tier-1 hook instead.

Headless runs

  • A blocked command does not fail the run: Claude -p, Codex exec and agy -p all exit 0 and report success. The denial is visible only in permission_denials[] (Claude JSON output), in denied_actions and stderr (agy), or in the model's reply (Codex).
  • Codex silently runs exec without the hook after the hook definition changes, until you trust it again via /hooks.
  • agy -p with --dangerously-skip-permissions auto-approves a gate ask/force_ask; a deny still blocks.
  • agy -p without that flag turns a command that actx neither rewrites nor decides into a soft refusal unless an allow rule of yours covers it. A control run without actx was not made: it needs edits under ~/.gemini/.
  • OpenCode: a user-typed !cmd bypasses plugins, so actx neither rewrites nor gates it.

actx init merges — it never overwrites your configuration files. actx init --show lists status, and actx init --uninstall removes only the actx entry.

Configuration

Created automatically on first run at ~/.config/actx/config.json:

{
  "tee": {
    "enabled": true,
    "mode": "failures",
    "dir": "~/.local/share/actx/tee"
  },
  "truncate": {
    "max_lines": 500,
    "max_line_chars": 300
  },
  "bypass_commands": [],
  "tracking": {
    "enabled": true,
    "history_days": 90
  }
}
  • tee.mode: failures (default), always, or never.
  • bypass_commands: list of command names (first token) that run unfiltered, e.g. ["git"]. Environment variable ACTX_BYPASS=1 bypasses filtering for the current call only.
  • A truncated result (line cap or long-line clip) always tees regardless of enabled/mode, and the tee path is printed.
  • Tee files are ~/.local/share/actx/tee/<unix_ts>_<sha1(command)[:8]>.log, kept to 100 files.

Analytics

Local-only, on by default. actx records every proxied command as an aggregate into a single SQLite file ~/.local/share/actx/history.db:

  • sha1(command) and category (first token) — never full commands or arguments;
  • strategy — which compression path ran (git.status, git.diff, ls, grep, read, test, passthrough, …);
  • bytes_before/after, exit code, timestamp.

The directory is 0700 and the database is 0600, so only your user can read it.

Views:

  • actx gain — total saved bytes, approximate tokens (bytes / 4), savings %, top categories and strategies.
  • actx gain --breakdown — full composition by strategy.
  • actx gain --graph|--history|--daily and --format json.
  • actx discover / actx session.
  • actx insights [--days N] [--top N] [--json] — orchestration analytics: repeated calls, failures, passthrough candidates, and conservative suggestions.
  • actx insights --verbose-commands — top passthrough heads by raw output volume with a suggested compact-flag convention (from the shared conventions table; heads without a convention show no advice). Reports are head-level: command_text never appears in the advisor.
  • actx insights adoption report — for the wave-1/2 heads (cloud CLIs, docker/kubectl/helm, mobile toolchains, data stack): share of compressed calls among actx-mediated calls (calls bypassing actx are not observed; internal refusals/timeouts, exit 124/125, are excluded from both terms).

The history database stores the local command text (command_text, capped at 4096 chars) while the tool is personal; it is planned to shrink back to sha1 only after learning from the data. See journal.md.

Control:

  • actx tracking off / on — persist the switch in config (tracking.enabled).
  • ACTX_TRACKING=0 actx ... — disable for one call, overriding config.
  • actx tracking status — show state, db path, and call count.
  • actx tracking clear — delete the database.
  • Rows older than tracking.history_days (default 90) are pruned on write; set 0 to keep forever.

Error/exception paths are not recorded (they produce no savings); tree is not recorded (no subprocess); passthrough calls are recorded under passthrough.

Failed cloud-CLI calls print an advisory stderr hint (auth: run login manually; rate limit: retry with a pause) — advisory only: exit codes are never altered, the hint never enters tee files, and contextual forms only (HTTP 401, rate limit) are matched, so a failing test line like assert response.status_code == 429 never triggers one.

Safety

  • Deterministic L7 Security Gatekeeper: Intercepts shell commands in PreToolUse hooks before execution. Blocks credential access (.env, ~/.ssh/id_rsa, AWS/GCP keys, Keychain), network exfiltration, pipe-to-interpreter, OS mutation (rm -rf /), and insecure package registries. Prompts for human confirmation (ask) on destructive git commands.
  • No shell=True, os.system, eval, or exec anywhere — every subprocess is an exec-array.
  • Auto-rewrite follows the allow-list; the rewriter rejects shell metacharacters, pipes, redirects, command substitution, and write flags (--fix, format, …).
  • No project configs are read; adapters write only to global user files.
  • No network calls, accounts, or telemetry.

Development

Run the full test suite (stdlib unittest only):

python3 -m unittest discover tests

The suite covers the rewriter security boundary, hook E2E, all filters, adapter installation, an AST security scan, the bypass escape hatch, lazy-import guards, and performance targets.

Non-goals

  • Windows
  • Lexer / compound-command rewrite; python3 or aws auto-rewrite
  • Broad mutator auto-rewrite beyond the narrow allow-list
  • Remote telemetry

Releasing

  1. Bump VERSION in actx_lib/cli.py, commit, and tag:

    git add actx_lib/cli.py
    git commit -m "Bump version to X.Y.Z"
    git tag vX.Y.Z
    git push origin master
    git push origin vX.Y.Z
  2. The update-tap workflow updates the tap formula automatically when the tag is pushed (.github/workflows/update-tap.yml). It needs a PAT with contents: write on ampersante/homebrew-actx, stored in the actx repo as the secret TAP_PAT (Settings → Secrets and variables → Actions).

    Manual fallback: edit Formula/actx.rb in ampersante/homebrew-actx, set the tag URL and sha256, then push.

  3. Update installed copies:

    brew update
    brew upgrade actx

brew update takes no arguments — brew update <tap> does not refresh a single tap in current Homebrew.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages