execkit-mcp
An MCP server (stdio) that exposes
execkit shell sessions to any MCP-capable agent - Claude Code,
Cursor, Gemini CLI, Codex, VS Code, Windsurf, and others.
Tools
| Tool | Args | Returns |
|---|---|---|
session_create |
transport ("local"/"ssh"/"docker"); for ssh: host (hostname, IP, or a Host alias from your ssh config), user, password or key_path, optional port, fingerprint (pin host key); for docker: container (running name/id); optional allow/deny command lists |
{ "session_id": "..." } |
session_exec |
session_id, command, optional budget (shape output: grep/tail/head/head_tail + max_chars), optional timeout_secs |
structured ExecResult JSON: command, stdout, stderr (split!), exit_code, duration_ms, cwd, truncated, timed_out, and a hint when output was truncated with no budget |
session_list |
none | [{ session_id, transport, idle_secs }] |
session_destroy |
session_id |
{ "destroyed": true } |
session_checkpoint |
session_id, optional label |
{ "checkpoint_id": "..." } |
session_checkpoints |
session_id |
[{ id, label, created }] |
session_restore |
session_id, optional checkpoint_id |
{ restored_to, files_changed } |
Sessions are stateful - cd/env persist across session_exec calls. Output
is ANSI-stripped, secret-redacted, and bounded; commands pass the optional policy
fence before running.
Commands are non-interactive: stdin is /dev/null, so use sudo -n and -y
flags. Each session_exec times out after timeout_secs (default 120, or
EXECKIT_MCP_EXEC_TIMEOUT; max 3600). On timeout execkit sends Ctrl-C and returns
timed_out: true with exit_code: 124; the session keeps its cwd and env. For
long jobs, start them in the background and poll:
{ "session_id": "...", "command": "nohup make release > /tmp/release.log 2>&1 &" }
{ "session_id": "...", "command": "tail -n 20 /tmp/release.log" }
A session is closed automatically when its shell exits (exit, or a failing
command under set -e) or when a timed-out command ignores Ctrl-C. Later calls
with that id get an unknown session_id tool error; session_list shows what is
still open.
Checkpoints (remote only)
On SSH/Docker sessions execkit can snapshot the workspace before changing commands and restore it on demand - a filesystem "undo." It undoes FILES only, never side effects (DB writes, network, installs).
Two requirements: git on the remote host, and an explicit workspace.
Without a workspace, checkpoints and auto-snapshot are disabled - execkit will
not default to the cwd or home directory (snapshotting $HOME is slow and
would capture secrets). Set workspace to the project dir you want undo for (use
$HOME explicitly if you really mean it).
Control it via session_create:
workspace(root - REQUIRED to enable checkpoints)auto_snapshot(default true; effective only with a workspace)paths(sub-dirs under the root)checkpoint_ignores(extra gitignore-style patterns; added to the built-in defaults:.git,node_modules, build dirs, caches,.ssh,.aws, ...)
If git is absent, auto-snapshot disables itself and checkpoint calls return a clear "install git on the remote" error.
Credential-shaped files (.env, .env.*, *.pem, *.key, id_rsa*,
id_ed25519*, .ssh, .aws, .gnupg, .netrc) are never snapshotted, even if a
checkpoint_ignores negation tries to include them. The shadow repo
(~/.execkit/ckpt-<token>.git on the remote) is deleted when the session ends.
WARNING: session_restore is destructive. The workspace ends up matching the
checkpoint: tracked files are reverted, files the checkpoint did not have are
removed (including ones a later checkpoint tracked), and ALL untracked files and
directories under the workspace are deleted (via git clean), not only files
created since the checkpoint. Do not restore if untracked files in the workspace
must be preserved.
Output budgets
Pass budget to session_exec (or output_budget to session_create for a
session default) to shape output and protect the agent's context window:
// keep the last 200 lines of a noisy build
{ "session_id": "...", "command": "npm run build",
"budget": { "keep": { "mode": "tail", "n": 200 } } }
// grep a 50k-line log for errors, with 2 lines of context
{ "session_id": "...", "command": "cat big.log",
"budget": { "grep": { "pattern": "error|fail", "context": 2 } } }
Shaping is line-based, client-side, and runs AFTER secret redaction; it never
changes the exit code or side effects. When applied, the result includes a
budget report: per-stream mode, lines_total, lines_kept.
Install
uvx execkit-mcp --version # zero-install: uv fetches and runs it
pip install execkit-mcp # the server binary, shipped as a wheel (no Rust toolchain)
cargo install execkit-mcp # ...or via cargo (or build from source: cargo build -p execkit-mcp)
Then check the install and your environment:
execkit-mcp --version
execkit-mcp doctor # reports audit, SSH, and Docker readiness
Wire it into an agent
execkit-mcp is a stdio MCP server - register the installed binary with your
client. The fastest way is to let it print the config with the right binary path
already filled in:
execkit-mcp setup claude # or: cursor | gemini | codex | vscode | windsurf
(cargo install puts the binary at ~/.cargo/bin/execkit-mcp; use the full path
if it isn't on the client's PATH.)
Claude Code - one command:
claude mcp add execkit -- execkit-mcp # add `-s user` to enable it everywhere
Cursor (~/.cursor/mcp.json), Gemini CLI (~/.gemini/settings.json) and
Windsurf (~/.codeium/windsurf/mcp_config.json) - add the same block
(setup codex and setup vscode print those clients' formats):
{
"mcpServers": {
"execkit": { "command": "execkit-mcp" }
}
}
With uv you can skip the install and use "command": "uvx", "args": ["execkit-mcp"]
instead.
To turn on auditing or other operator settings, add an env block:
{
"mcpServers": {
"execkit": {
"command": "execkit-mcp",
"env": { "EXECKIT_MCP_AUDIT": "/var/log/execkit.jsonl" }
}
}
}
Then the agent can call session_create -> session_exec -> session_destroy.
Example session (what the agent sees)
// session_create {"transport":"local"} -> {"session_id":"a3f9-1_local"}
// session_exec {"session_id":"a3f9-1_local","command":"npm run build"}
// -> {"command":"npm run build","stdout":"...","stderr":"Error: Cannot find module 'webpack'",
// "exit_code":1,"duration_ms":3420,"cwd":"/home/u/app","truncated":false,"timed_out":false}
Security model
The agent driving these tools can be prompt-injected, so tool arguments are untrusted. Anything dangerous to the host/filesystem is therefore controlled by the operator at startup (env vars), not by per-call agent arguments:
| Env var | Purpose | Default |
|---|---|---|
EXECKIT_MCP_AUDIT |
append a JSONL audit log of every command here | off |
EXECKIT_MCP_AUDIT_DIR |
write one JSONL file per session into this directory (<session_id>-<open_ms>.jsonl); takes precedence over EXECKIT_MCP_AUDIT when both are set |
off |
EXECKIT_MCP_AUDIT_RETENTION_DAYS |
delete per-session log files older than this many days at startup (dir mode only); 0 disables |
14 |
EXECKIT_MCP_EXEC_TIMEOUT |
default session_exec timeout in seconds (clamped 1-3600); timeout_secs overrides per call |
120 |
EXECKIT_MCP_KEY_DIR |
SSH key_path must canonicalize to inside this dir; its config file is read for Host aliases |
~/.ssh |
EXECKIT_MCP_KNOWN_HOSTS |
execkit-managed SSH host-key verification file (TOFU; rejects changed keys) | ~/.execkit/known_hosts |
EXECKIT_MCP_INSECURE_ACCEPT_ANY_HOSTKEY |
DANGEROUS - disable host-key checks | unset |
EXECKIT_MCP_MAX_SESSIONS |
soft cap on concurrent live sessions | 64 |
EXECKIT_MCP_SESSION_TTL |
reap sessions idle longer than this many seconds (frees the process + cap slot); 0 disables |
1800 (30 min) |
EXECKIT_MCP_POLICY_FILE |
JSON allow/deny (program names) + deny_patterns (regex) the agent cannot edit; advisory |
off |
EXECKIT_MCP_WATCH_WEB |
Start the read-only browser viewer (loopback + URL token) and surface its URL via an MCP notification; the URL is stable across restarts so an open tab reconnects | off |
EXECKIT_MCP_WATCH_PORT |
Port for the viewer (the URL must stay stable for reconnect); falls back to a random port if taken | 7878 |
EXECKIT_MCP_WATCH_OPEN |
Also auto-open the browser at the viewer URL (default: just show the link) | off |
- Host keys are verified by default (TOFU against execkit's own known_hosts,
~/.execkit/known_hosts, keyedhostor[host]:port; a changed key is rejected as a likely MITM). Your OpenSSH~/.ssh/known_hostsis not used. IfEXECKIT_MCP_KNOWN_HOSTSpoints at an OpenSSH-format file, connecting to a new host fails with an error instead of writing to it. Pass afingerprintto require an exact key, or set the insecure env var only for throwaway/test hosts. key_pathis sandboxed toEXECKIT_MCP_KEY_DIR; out-of-bounds/traversal paths are rejected with a generic error (no path-existence leak).- Audit destination is operator-chosen, never a tool argument (prevents an injected agent from writing to arbitrary files).
- Docker sessions run
docker execagainst any container the daemon can see, so the agent reaches whatever yourdockercontext exposes. Grant the server Docker access only when you want that, and scope the daemon/context accordingly. - The server speaks MCP on stdout; all diagnostics go to stderr.
- Use
allow/denyfor a command fence, and run the agent + SSH user with least privilege. The fence is advisory - defense in depth, not a sandbox.
Watch live activity (read-only)
Point EXECKIT_MCP_AUDIT at a file, then watch it from another terminal:
execkit-mcp watch /var/log/execkit.jsonl # or just: execkit-mcp watch (uses $EXECKIT_MCP_AUDIT)
watch also accepts a directory - it tails every *.jsonl file in it and picks
up new session files as they appear:
execkit-mcp watch /var/log/execkit/ # or: execkit-mcp watch (uses $EXECKIT_MCP_AUDIT_DIR)
watch is a live, read-only TUI: the agent's sessions on the left, the selected
session's shell transcript on the right (prompt, command, stdout, stderr in red,
exit status) - rendered like a normal shell, not JSON. Switch sessions with 1-9
or the arrow keys, scroll with PgUp/PgDn, quit with q. It only ever reads the
log and never touches a session. Because the data comes from the server (not the
client), it works the same under any MCP client (Claude Code, Cursor, Gemini, ...).
For a headless or background view - no terminal required, pipeable - use
--follow instead of the TUI. It prints each command and its output as a line,
prefixed with the session id, as it happens:
execkit-mcp watch --follow /var/log/execkit/
# [a3f9-1_local] /home/u $ npm run build
# [a3f9-1_local] x exit 1 (3420ms)
# [a3f9-2_ssh_deploy@web-01] /srv $ systemctl restart app
Session ids are self-identifying - <run>-<n>_local,
<run>-<n>_ssh_<user>@<host>[:port], or <run>-<n>_docker_<container>, where
<run> is a short random prefix per server process - so the audit filenames and the stream read clearly
at a glance.
Live viewer in your browser
execkit-mcp watch --serve [--open] <audit-file-or-dir> serves the transcript as
a local web page (127.0.0.1 only, single-use URL token in the link). Add --open
to launch your browser at it. The MCP server also starts it automatically when
EXECKIT_MCP_WATCH_WEB is set (it prints the link and pushes it as a
notification; the link is stable across restarts so an open tab reconnects).
Reading the page:
- Sidebar - sessions grouped by transport (
local,ssh,docker) in collapsible accordions. The number on a group header is the count of sessions in it (ssh (2)); the number on a session row is its command count - how many commands ran (deploy@web-01 (7)), with the exact count on hover. The active session is highlighted with a dot. - Transcript colors (legend in the header): the prompt line
cwd $ commandis cyan (cmd), stdout is default (out), stderr and failing exits are red (err), a clean exit is green (ok), and session/markers are dimmed. Each command shows its exit code and duration. Click a legend item to show/hide that line type (e.g. clickoutto hide stdout and skim just the commands and errors). - Search - press
/to find within the current transcript; matches are highlighted with a runningn / mcount,Enter/Shift+Enter(or the next/prev buttons) step through them, andEscclears. Presse(or "next err") to jump to the next error/blocked line. - Bottom status bar - the connection state (
connected/disconnected - retrying) on the left. On the right it shows details of the currently selected session - its id, transport, command count, live/closed state, and absolute last-activity time - and stays there as you browse. Click it to copy the session id. An action (e.g.Exported 1_local.md,Renamed to ...,Pinned to top) flashes briefly, then the bar reverts to the session's details. - History - past sessions from
EXECKIT_MCP_AUDIT_DIR(one file per session) appear under "History", newest first, each with a relative time (5m ago; hover for the absolute last-activity timestamp); click one to view its transcript read-only. The list and last-activity times refresh automatically. Requires dir-mode auditing; with a singleEXECKIT_MCP_AUDITfile there is no per-session history. - Per-session actions (the 3-dots menu on a row): Rename (inline edit; a
display alias only), Pin to the top, Keep (retain in history past the
trim limit), Export to
.txt/.log/.md/.json, and Screenshot to.png.
Renames, pins, keeps, and the sidebar width persist in ~/.execkit/viewer-state.json
(mode 0600). That file is the viewer's only write surface: it holds display
metadata only and can never affect a session, command, or the audit log. The page
itself is read-only - it only reads the audit stream and that one metadata file.
In the agent's client (no terminal, no audit log)
The server also pushes each command to your MCP client as it runs, as standard
MCP notifications - so a host agent can show its own shell activity live without
anyone opening a watch terminal. Every session_exec emits:
- a log notification (
notifications/message) carrying the full shell transcript -infoon success,warningon a non-zero exit; and - a progress notification (
notifications/progress) with a one-line summary, when the client supplied aprogressTokenfor the call.
This needs no EXECKIT_MCP_AUDIT* setup - the server advertises the logging
capability and streams unconditionally. It reveals nothing new: the client
already receives the same stdout/stderr in the tool result, redacted and bounded.
How (or whether) the activity is surfaced is up to the client.
Operator command policy (advisory)
Point EXECKIT_MCP_POLICY_FILE at a JSON file to set an allow/deny fence the
agent cannot edit (unlike the per-call allow/deny, which the agent supplies):
{
"allow": ["git", "ls", "npm"],
"deny": ["rm", "dd", "shutdown"],
"deny_patterns": ["\\brm\\b", "kubectl\\s+delete", "git\\s+push\\s+.*--force"]
}
allow(program names): if non-empty, only these may run. Empty/absent = all.deny(program names): always blocked; deny wins over allow.deny_patterns(regex over the whole command): for what names cannot express.
Prefer a deny_pattern over a name deny for anything that matters: name
matching only sees the program name per pipeline segment, so deny: ["rm"] misses
sudo rm and xargs rm, while deny_patterns: ["\\brm\\b"] catches them. In JSON
the regex backslashes double up (\\b); use (?i) for case-insensitive matching.
A blocked command never runs; it is recorded in the audit log, shown in watch,
and pushed to the client as a warning. This is an ADVISORY guardrail, not a
sandbox: string matching is trivially bypassable (env curl gets past
deny: ["curl"], as do base64 and bash -c). A denial names the rule or pattern
that matched. The real boundary is a least-privilege user, a container, or a scoped SSH account.
Apache-2.0.
Release files for execkit-mcp 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| execkit_mcp-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| execkit_mcp-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| execkit_mcp-0.9.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| execkit_mcp-0.9.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 15.2 MB
Release files / execkit_mcp-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | execkit_mcp-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
f78c6db888e1ad3d4250fba9c9cb4908cc3006d3bd5b772cc716c3a6a701e4ae
|
|
BLAKE2b-256 checksum How to use checksums |
9db29e5e7444f72cc9b6902e6b12903db2fbf19042357c97973d6b59080ed862
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / execkit_mcp-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | execkit_mcp-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 3.5 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
418794d8c1df664fad38df2ec86d92d398c69e94669226d8f67bdcfc90cf8919
|
|
BLAKE2b-256 checksum How to use checksums |
e22bd9a2869d971418ce392edfe1870388f73448068407e08a838d88c2df59fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / execkit_mcp-0.9.0-py3-none-macosx_11_0_arm64.whl
| Download URL | execkit_mcp-0.9.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.7 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5751818d43b58de188d058f4531e6cef1de46c59196f25da4ece06222164a469
|
|
BLAKE2b-256 checksum How to use checksums |
837e17159706f21e55d7023e16083275e1f57ce9da4cc97e6f61ddc886f0883f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / execkit_mcp-0.9.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | execkit_mcp-0.9.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 3.9 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
222acc09bcab414f86891fc250aefb0f0f4e39a6c55925532c3d94ae0ebbc8f3
|
|
BLAKE2b-256 checksum How to use checksums |
76fae2ea59d1771c87b4d4512167d008a031637d1508eaa64070967a1e3dd43e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log