🛡️🔒📜 A CLI security boundary for privileged ops, escape-hatch-resistant and with an audit trail.
  • Go 97.3%
  • Python 1.4%
  • Go Template 0.5%
  • Makefile 0.3%
  • Shell 0.3%
  • Other 0.2%
Find a file
2026-05-29 19:46:40 +00:00
.agents/skills feat(dispatch): invert surface-to-isolation mapping via cli-guard bump 2026-05-28 04:01:11 -07:00
.claude lockdown: sync to coily v2.47.0 [skip ci] 2026-05-28 11:15:11 +00:00
.coily chore(catalog): migrate dependsOn to scheme-less URLs, drop providesApis 2026-05-27 21:38:39 -07:00
.forgejo/workflows feat: add sync-lockdown release job, fix reserved FORGEJO_PAT secret name 2026-05-28 02:06:19 -07:00
.github/workflows release: port pipeline from github Actions to forgejo Actions 2026-05-25 20:42:18 -07:00
cmd/coily refactor: wipe agent-guard peer references, point to ward 2026-05-29 12:45:53 -07:00
docs refactor: wipe agent-guard peer references, point to ward 2026-05-29 12:45:53 -07:00
Formula chore(formula): bump to v2.50.0 [skip ci] 2026-05-29 19:46:40 +00:00
scripts feat: add sync-lockdown release job, fix reserved FORGEJO_PAT secret name 2026-05-28 02:06:19 -07:00
skills/coily-passthroughs feat(forgejo): add repo topics list/set + repo view --json 2026-05-28 06:05:35 -07:00
test/integration Initial commit: coily operator CLI with AI-agent safety boundary 2026-04-22 14:47:07 -07:00
.gitignore chore: migrate skill directory to .agents/skills 2026-05-22 00:11:24 -07:00
.golangci.yml Add OpenAPI->coily generator; mount modio/glama/discord/sentry/trello-api under ops 2026-05-07 23:26:52 -07:00
.pre-commit-config.yaml chore(catalog): migrate dependsOn to scheme-less URLs, drop providesApis 2026-05-27 21:38:39 -07:00
.pre-commit-hooks.yaml add coily lint subcommand for .coily/coily.yaml ↔ Makefile drift 2026-05-11 17:19:08 -07:00
AGENTS.md release: port pipeline from github Actions to forgejo Actions 2026-05-25 20:42:18 -07:00
config.example.yaml feat(channel): add coily channel CLI for Agent Channel coordination 2026-05-22 21:44:44 -07:00
CONTRIBUTING.md refactor: wipe agent-guard peer references, point to ward 2026-05-29 12:45:53 -07:00
go.mod chore(deps): bump cli-guard for detached-dispatch startup-crash guard 2026-05-28 06:32:10 -07:00
go.sum chore(deps): bump cli-guard for detached-dispatch startup-crash guard 2026-05-28 06:32:10 -07:00
Makefile build: GO_TEST falls back to go test when gotest absent, closes #265 2026-05-19 17:29:54 -07:00
README.md refactor: wipe agent-guard peer references, point to ward 2026-05-29 12:45:53 -07:00
SECURITY.md feat(ops-forgejo): pass titles verbatim, drop cosmeticSanitize mangle 2026-05-28 04:26:56 -07:00

coily

🛡️ coily is a CLI security boundary for privileged ops, 🔒 escape-hatch-resistant and with an 📜 audit trail.

Operator CLI for Kai's homelab (kai-server, coilysiren k3s cluster, AWS / Tailscale).

$ coily whoami                                  # unified view across aws/gh/kubectl
$ coily ops aws ssm get-parameter --name '/x; cat /etc/passwd'
Error: policy: shell metacharacter rejected: arg name contains ";" at index 18

Both invocations land in ~/.coily/audit/<repo>.jsonl as structured rows.

This repo exists for three reasons.

  1. One audited surface for every privileged tool. External pass-throughs (coily ops aws/gh/kubectl ...), standalone pass-throughs (coily docker, coily tailscale), and package managers (coily pkg pnpm/uv/cargo/brew) all forward verbatim, gated by argv shell-metacharacter rejection and an audit row. coily brew is a separate top-level for the four mutating brew verbs, scoped to coilysiren/tap/* by default.
  2. Safety boundary for AI agents. Claude Code's deny: "Bash(kubectl delete:*)" is prefix-matched and bypassed by sh -c, env, python -c subprocess, Makefile shell-outs, etc. coily inverts the model: a narrow allowlist (Bash(coily:*)) funnels privileged ops through one Go binary that re-validates structured args and rejects metacharacter injection. Full reasoning in SECURITY.md.
  3. Auditors love to see me coming. Every invocation is appended to a structured JSONL log with session metadata.

Install

coily is never published as a prebuilt binary; every install is a local build. Canonical: make install sudo-installs to root-owned /usr/local/bin. Brew tap is bootstrap-only (still build-from-source, doesn't preserve root-owned property).

make install            # macOS, sudo-install /usr/local/bin/coily
brew tap coilysiren/coily https://forgejo.coilysiren.me/coilysiren/coily && brew install coilysiren/coily/coily
make install-windows    # elevated shell, installs C:\Program Files\coily\coily.exe
make deploy-server      # linux cross-compile + scp + sudo-install on kai-server
make dev                # ./bin/coily-dev for iteration (not on PATH)

make test defaults to gotest. To opt out: make test GO_TEST="go test". The agent's allowlist trusts coily, not coily-dev. Dev builds use -tags dev, prod uses -tags prod.

aws, kubectl, gh resolution

Via $PATH like any other binary. Argv validation + audit + the lockdown deny list carry the safety boundary; binary authenticity is the host's problem. See SECURITY.md.

Per-repo commands (.coily/coily.yaml)

Each repo drops a coily.yaml inside .coily/ to declare its dev verbs. coily exec test, coily exec lint, etc. Replaces per-repo Makefiles and pyinvoke tasks without widening the boundary.

commands:
  test: go test ./...
  lint:
    run: golangci-lint run ./...
    description: Lint with golangci-lint.
  • Walks up from cwd to find .coily/coily.yaml. $COILY_REPO_CONFIG overrides.
  • coily --list prints built-ins + repo commands. coily exec <cmd> --help shows the expansion.
  • Every token passes policy.ValidateArg. Metacharacters rejected at load time and at invocation.
  • Audit verb is repo.<cmd>. Same log file.
  • Repo commands sit under coily exec, can't shadow built-ins.

Architectural decisions

  • Single binary, single trust boundary. One allowlist entry, Bash(coily:*).
  • Trust $PATH for sub-tool binaries. Earlier sha256-pinned manifest dropped: the protection didn't justify the release pipeline.
  • SDK-native for simple APIs. ssh/scp + tailscale via Go SDKs. No subprocess means no argv to a shell.
  • Mirror the sub-CLIs exactly. coily ops aws ssm get-parameter takes the same args as aws ssm get-parameter.
  • Config is embedded. Changes require rebuild + sudo install.
  • No self-update. Binary cannot rewrite itself. v2 plan around adversarial-reviewed CI installs in SECURITY.md.
  • No coily shell / coily run escape hatch.

Prior art

coily is a personal-scale remix of Teleport (scoped invocation + JSONL audit), mise (one CLI multiplexing many tools), and Dagger (validate structured args, don't just shell out).

Contributing

coily does not accept external pull requests - it is a security boundary, and an external change is an attack surface. See CONTRIBUTING.md; contribute to ward instead. Issues welcome.

See also

Cross-reference convention from coilysiren/agentic-os#59.