Imported from cairijun/codecompanion-run-bash.nvim (
AGENTS.md). Install upstream withnpx skills add cairijun/codecompanion-run-bash.nvim. Copyright stays with the author.
AGENTS.md
Coding agent guide for codecompanion-run-bash.nvim.
What
CodeCompanion extension. Replaces built-in run_command tool. Execute bash in chat. Two goals:
- Security — sandlock sandbox (real boundary) + pause list approval (human checkpoint). Minimize approval friction, keep safety.
- Background processes — built-in
run_commandhangs oncmd &(vim.system waits for pipe close, never returns while process alive). This extension: dedicated background mode. Command detached, returns session_id + partial output. Agent kills later via{"action": "kill", "session_id": "..."}.
Design Principles
- Sandbox = real security boundary. Landlock + seccomp isolate fs + syscalls. Pause list NOT security — just human checkpoint for risky-but-sandboxable ops.
- Minimize approval fatigue. Frequent approval → user rubber-stamp or disable review → worse than no review. Safe commands = zero friction. Only pause-listed commands pause.
- Agent use sandbox by default. Skip sandbox = rare, needs human approval every time.
Architecture
Major files in lua/codecompanion/_extensions/run_bash/:
| File | Role |
|---|---|
init.lua |
Entry point. Config merge (incl. legacy→new migration), tool registration, approval callback wiring, cleanup on VimLeavePre. |
checker.lua |
Pause list engine. Treesitter parse bash, resolve proxy commands, match against rules. |
sandbox/init.lua |
Facade. Backend name validation, sandbox_name generation, run/kill/is_available/should_use/get_description dispatch by opts.backend. |
sandbox/resolver.lua |
Generic path resolution: resolve_path, resolve_fs_rules, XDG fallback. |
sandbox/backends/sandlock.lua |
sandlock backend: CLI arg construction, availability (sandlock exec + profile), validate_opts, named-sandbox run/kill. |
sandbox/backends/bubblewrap.lua |
bubblewrap backend: maps fs_* rules to bwrap CLI (--bind, --ro-bind, --dev-bind for device nodes — bwrap has no read-only device bind, so validate_opts rejects readable-only device rules and devices whose nearest bound ancestor mount is read-only, --tmpfs for dirs only), uid_map availability check, two-stage SIGTERM/SIGKILL kill. |
tool.lua |
Tool definition. Schema, dynamic description (from sandbox.get_description), output handlers. Session registry stores sandbox_opts + sandbox_name; kill passes them through the facade. |
Flow: init.setup() registers tool → agent calls tool → handler validates args → sandbox facade decides + dispatches to backend → on_exit → output → chat.
Backend interface contract (each backend must implement):
is_available(opts) -> booleanvalidate_opts(opts, rules) -> string|nil(error message or nil;rulesare the rawfs_*rules, for backends that must reject rule combinations at setup)capabilities() -> { named_sandbox }get_description() -> stringrun(opts, exec_params) -> handle|nil, pid|string|nil, sandbox_used:booleankill(opts, sandbox_name, pid, on_killed, deps) -> nil
The facade returns a 4-tuple handle, pid, sandbox_used, sandbox_name from run() where sandbox_name is non-nil only for backends with named_sandbox = true.
Approval Logic
action=kill→ auto-approve.- Non-sandbox → always require approval.
- Sandbox → check pause list. Parse failure → conservative: require approval.
Conventions
- Lua, stylua formatted.
make format. - Comments explain current code intent/constraints — not task steps or change rationale.
- Temp files mode 0600, removed on exit.
- uv.spawn handles
unref'd — don't block event loop.
Dev
make deps # install test deps
make test # all tests
make test_file FILE=tests/units/test_checker.lua # single file
make format # stylua
make clean # remove deps
Test layers:
- Unit — checker (
tests/units/test_checker.lua): Pause list logic, config override, edge cases. Pure logic, no I/O. - Unit — resolver (
tests/units/test_resolver.lua): Path resolution, XDG fallbacks,resolve_fs_rulesgrouping/dedup/existence checks. Testssandbox/resolver.luain isolation. - Unit — sandlock backend (
tests/units/test_backend_sandlock.lua): CLI args, availability, validate_opts, run/kill spies. Testssandbox/backends/sandlock.luain isolation. - Unit — bubblewrap backend (
tests/units/test_backend_bubblewrap.lua): Bind/connect args mapping,--dev-binddevice routing, setup-time device rule validation, fs_denied dir-vs-file-skip, uid_map availability, two-stage kill. Testssandbox/backends/bubblewrap.luain isolation. - Unit — sandbox facade (
tests/units/test_sandbox.lua): Facade dispatch byopts.backend, unknown backend error,run()return shape, defaults, and non-sandbox two-stage kill. Testssandbox/init.luawithout real backends. - Unit — sandbox backends matrix (
tests/units/test_sandbox_backends.lua): Common backend contract (execution, capture, exit codes, isolation, device rules, kill) againstsandlock,bubblewrap, and the non-sandboxnonedriver. - Unit — tool (
tests/units/test_tool.lua): Resource cleanup, registry persistence ofsandbox_opts/sandbox_name, kill opts dispatch, cleanup_all per-entry, dynamic description, temp file security, concurrency safety, async I/O, ANSI stripping. Teststool.luawith mockedsandboxfacade. - Unit — init (
tests/units/test_init.lua): Config merge, defaultbackend="sandlock", legacy→new migration,validate_backend_opts, requirement of approval flow. Testsinit.setup()in isolation. - Integration (
tests/test_integration.lua): FullChat → run_bash → sandbox → commandpipeline. Only the LLM Adapter is mocked. Tests the contract between run_bash and CodeCompanion — tool registration, approval flow, execution, output formatting — all through the Chat interface, not direct handler calls.
Testing guidelines
- Prefer real implementations over mocks.
- Extract pure functions and test them without mocks.
- Use dependency injection or local stubs instead of global replacements.
- If a global mock is unavoidable, wrap it with
Helpers.with_mocksso restoration is guaranteed even on failure. - Reserve global mocks for external boundaries that cannot be injected (e.g., LLM adapter in integration tests).
Boundary: If a test can pass by calling tool.create(), handler(), or require_approval_before() directly, it belongs in a unit test. Integration tests MUST exercise the Chat interface.
Agent test caveat
When using run_bash to run tests: run_bash sandbox enabled by default — sandlock can't nest. Two ways:
"skip_sandbox": true— test outside of sandbox (no nesting conflict).TEST_CC_RUN_BASH_SANDBOX_BACKENDS=""— skip all backend-gated tests (thenonebaseline is also skipped). Use when running tests inside a nested sandbox or when backends are missing. DON'T when full backend coverage is needed.TEST_CC_RUN_BASH_SANDBOX_BACKENDS="sandlock"— run only the sandlock row of the backend matrix.