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
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.
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
- 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 (
gitRO,ls/grep/find/wc/…, test runners, linters without write flags,docker/kubectl/ghRO, …) 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; nopython3/awsauto-rewrite. - Exact exit codes — the original command's exit code is preserved, including
git statusreturning128outside a repo andgrepreturning1for 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 --failuresstays silent on a green run). - Lossless filenames —
git status,ls, andfindcompress the format, not the names. - Tee for recovery — truncated or failed output is saved to
~/.local/share/actx/teeas 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=1or abypass_commandsentry 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.
Requirements: Python 3.14 (macOS or Linux). No third-party packages.
chmod +x actx
# Put `actx` on PATH, e.g. symlink into a bin directory:
ln -s "$(pwd)/actx" ~/.local/bin/actxThen 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 piAfter installation, restart the agent.
curl -fsSL https://github.com/ampersante/actx/archive/refs/tags/v2.14.0.tar.gz | tar xz
cd actx-2.14.0
bash install.shKeep the extracted directory — install.sh symlinks the actx binary from it.
brew tap ampersante/actx
brew trust ampersante/actx
brew install actxbrew installs Python 3.14 automatically via the formula dependency.
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 HomebrewIf 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/actxIf 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.
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.
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 valuespython3 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.
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.
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
permissionrules 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 asshell:git push*still match them. OpenCode has no confirmation channel for plugins, so a gateask(e.g.git push --force) becomes a deny with the reasonactx security gate (opencode: no confirmation channel) …. Not covered: user-typed!cmd, theexecutetool (code-mode bridge for catalog/MCP tools), MCP tools. The agentic-kitapplywrites the same plugin file; until the kit drops its template, re-runactx initafter a kit apply. - Devin also checks its rules after the hook. Git mutators are allowed without being rewritten, so a matching
denyrule of yours still wins. A gateaskprompts;devin -prejects it. Mutator forms actx does not rewrite (git -C dir push) get Devin's default prompt, which-prejects.~/.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 initleaves it untouched, exits 1 and prints the entry to add by hand. - pi never asks the human: a gate
askbecomes a deny with the reasonactx 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 toactx git log …. - Devin with the Tier-2 rules file only (no hook):
devin -prejects a rewrittenactx git statusas "requires confirmation"; add an allow rule foractx *. Since actx 2.13.0 Devin is wired as a Tier-1 hook instead.
- A blocked command does not fail the run: Claude
-p, Codexexecandagy -pall exit 0 and report success. The denial is visible only inpermission_denials[](Claude JSON output), indenied_actionsand stderr (agy), or in the model's reply (Codex). - Codex silently runs
execwithout the hook after the hook definition changes, until you trust it again via/hooks. agy -pwith--dangerously-skip-permissionsauto-approves a gateask/force_ask; adenystill blocks.agy -pwithout 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
!cmdbypasses 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.
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, ornever.bypass_commands: list of command names (first token) that run unfiltered, e.g.["git"]. Environment variableACTX_BYPASS=1bypasses 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.
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)andcategory(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|--dailyand--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_textnever appears in the advisor.actx insightsadoption 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; set0to 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.
- 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, orexecanywhere — 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.
Run the full test suite (stdlib unittest only):
python3 -m unittest discover testsThe 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.
- Windows
- Lexer / compound-command rewrite;
python3orawsauto-rewrite - Broad mutator auto-rewrite beyond the narrow allow-list
- Remote telemetry
-
Bump
VERSIONinactx_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 -
The
update-tapworkflow updates the tap formula automatically when the tag is pushed (.github/workflows/update-tap.yml). It needs a PAT withcontents: writeonampersante/homebrew-actx, stored in theactxrepo as the secretTAP_PAT(Settings → Secrets and variables → Actions).Manual fallback: edit
Formula/actx.rbinampersante/homebrew-actx, set the tag URL and sha256, then push. -
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.
MIT. See LICENSE.