hardened-terminal-mcp
This server provides a security-hardened way for AI models to execute a limited, allowlisted set of terminal commands within a sandboxed environment.
run_command: Safe execution of allowlisted commands with optional
cwdjailed to a configured root.explain_command: Dry-run that returns the policy verdict without executing the command.
Structured response: Both return a
CommandResultwithok,code(ResultCode likeOK,POLICY_DENIED,TIMEOUT,CWD_ESCAPE,AUDIT_UNAVAILABLE),stdout,stderr,exit_code,duration_ms, andpolicy_reason.Policy enforcement: Deny-by-default; explicit allowlists, denylists, and argument-specific denials (
deny_args).Sandboxing: No shell execution (preventing metacharacter injection), directory jail with escape prevention, time and output limits, environment scrubbing (only
PATH,HOME,LANG).Secrets redaction: Automatic redaction of common secret patterns with
[REDACTED:<kind>]markers.Auditing: Append-only audit trail with fail‑closed mechanism ensuring no unlogged execution.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hardened-terminal-mcplist files in the current directory"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hardened-terminal-mcp
A security-hardened MCP server that lets an AI model run a small, explicitly allowlisted set of terminal commands — safely.
Why
Giving a model raw shell access is dangerous: one crafted string can chain commands, read secrets, or escape the working directory. This server turns "let the model run commands" into a bounded, auditable operation instead of an open door. Every command is checked against a deny-by-default policy, run with no shell, jailed to one directory, time- and output-bounded, and logged to an append-only audit trail before it is allowed to run.
Related MCP server: Shell-MCP
Demo
Real output, captured from a jail with ls, cat, echo allowlisted and rm denied:
>>> explain_command("ls -la")
{
"ok": true,
"code": "OK",
"stdout": "",
"stderr": "",
"exit_code": null,
"duration_ms": 0,
"policy_reason": "command 'ls' is allowed"
}
>>> run_command("ls -la")
{
"ok": true,
"code": "OK",
"stdout": "total 44\ndrwxrwxr-x 2 asaad asaad 4096 Jul 30 18:36 .\ndrwxrwxrwt 24 root root 36864 Jul 30 18:36 ..\n",
"stderr": "",
"exit_code": 0,
"duration_ms": 4,
"policy_reason": "command 'ls' is allowed"
}
>>> run_command("rm -rf /")
{
"ok": false,
"code": "POLICY_DENIED",
"stdout": "",
"stderr": "",
"exit_code": null,
"duration_ms": 0,
"policy_reason": "command 'rm' is on the denylist"
}
>>> run_command("cat /etc/passwd", cwd="/etc")
{
"ok": false,
"code": "CWD_ESCAPE",
"stdout": "",
"stderr": "",
"exit_code": null,
"duration_ms": 0,
"policy_reason": "cwd escapes jail root: '/etc'"
}Quickstart
Requires Python 3.12+. Install with uv:
uv syncWrite a minimal policy.toml:
jail_root = "/srv/htmcp/sandbox" # commands are jailed here; REQUIRED
[allowlist]
commands = ["ls", "cat", "echo"]
[denylist]
commands = ["rm"]Register it with your MCP client:
{
"mcpServers": {
"hardened-terminal": {
"command": "uv",
"args": ["run", "hardened-terminal-mcp"],
"env": {
"HTMCP_JAIL_ROOT": "/srv/htmcp/sandbox",
"HTMCP_POLICY_FILE": "/srv/htmcp/policy.toml"
}
}
}
}How it works
flowchart LR
client([MCP client / model]) -->|run_command / explain_command| server[server.py]
server -->|explain| policy[policy engine<br/>deny-by-default, no shell]
policy -->|deny| result[CommandResult envelope]
policy -->|allow| jail{cwd inside jail?}
jail -->|escape| result
jail -->|ok| exec[executor<br/>shell=False, env scrub, timeout, cap]
exec --> redact[redaction<br/>before truncation]
redact --> result
result --> client
server -. attempt record<br/>BEFORE execution .-> audit[(append-only<br/>audit.jsonl)]
server -. outcome record<br/>AFTER execution .-> auditThe audit log is a side branch, not on the return path: the attempt record is
written before the executor runs, and the outcome record after. explain_command
follows the same path with execution skipped.
Security guarantees
Guarantee | How it is enforced |
Deny by default | Command name must be on the allowlist; denylist always wins; a name not listed is denied |
No shell |
|
cwd jail | Requested cwd is resolved (normalising |
Timeout | Wall-clock |
Output caps | stdout/stderr byte-capped with a truncation marker; redaction runs before the cap so a secret can't be split |
Env scrub | Child gets only an allowlisted env ( |
Audit fail-closed | An attempt record is written before execution; if it can't be written under fail-closed, the command does not run ( |
Redaction | Secrets in output and in audit |
Threat model
Does not defend against (out of scope by design):
A dangerous command you allowlisted. If you allow an interpreter or a shell-like tool (
bash,python,sh,find -exec,awk,env, …), the model can do anything that tool can. Policy strength is entirely the operator's allowlist.Kernel / sandbox escapes. The jail is a path-containment check, not a kernel sandbox — there are no namespaces, cgroups, or seccomp. A local-privilege or kernel exploit reachable from an allowlisted binary is not contained.
Host access to the audit log. The trail is tamper-evident to the server, not tamper-proof. Anyone with filesystem access to
audit.jsonlcan read, alter, or delete it.Redaction completeness. Redaction is pattern-based and best-effort; a novel secret format the patterns don't recognise can pass through.
Compared to a naive terminal MCP server
Many quick MCP servers wrap subprocess.run(cmd, shell=True). That is convenient
and unsafe. This table is factual, not a claim of perfect security.
Concern |
| hardened-terminal-mcp |
Command surface | Any command the shell can parse | Only allowlisted names; denylist wins |
Shell metacharacters | Interpreted ( | Rejected before evaluation; no shell |
Working directory | Wherever the process is | Pinned to a required jail root; escapes refused |
Environment | Full parent env (secrets included) | Scrubbed to |
Secrets in output/args | Passed through | Pattern-redacted before return and before audit |
Auditability | None by default | Append-only JSONL; fail-closed by default |
Reference
Both tools return a CommandResult:
CommandResult {
ok: bool # true only for OK / OUTPUT_TRUNCATED
code: ResultCode # see taxonomy below
stdout: str # redacted; empty for explain / non-executing paths
stderr: str # redacted
exit_code: int | null # process exit code; null when nothing ran
duration_ms: int
policy_reason: str | null # human-readable allow/deny reason
}run_command(command: str, cwd: str | None = None)— evaluatecommandagainst policy and, if allowed, run it sandboxed.cwd(if given) must resolve inside the jail root, elseCWD_ESCAPE. Every call is audited.explain_command(command: str)— dry run: return the policy verdict only, with emptystdout/stderrandexit_code = null. It never reaches the executor.
Each code maps to exactly one condition.
Code | Meaning |
| Executed and returned ( |
| Denied by policy (list, arg rule, shell metacharacter, empty) |
| Command could not be parsed into an argv |
| Wall-clock timeout; the process was killed |
| Ran, but output hit the byte cap |
| Requested cwd escaped the jail root |
| Allowed command couldn't run (not found, permission, etc.) |
| Fail-closed: the audit record couldn't be written; not run |
| Unexpected server error; generic message only, no traceback |
Variable | Meaning | Default |
| Path to |
|
| Jail directory (required via this or | — (refuse if unset) |
| Audit trail path |
|
|
|
|
Env values override the file. Startup fails loudly on a missing/invalid policy, an unset or non-directory jail root, an invalid operator regex, or an audit log located inside the jail root.
# jail_root is REQUIRED (here or via HTMCP_JAIL_ROOT). Keep the audit log OUTSIDE it.
jail_root = "/srv/htmcp/sandbox"
audit_log = "/var/log/htmcp/audit.jsonl"
audit_fail_mode = "closed" # "closed" = don't run if unloggable; "open" = run + warn
[allowlist]
commands = ["ls", "cat", "echo", "git"]
[denylist]
commands = ["rm", "shutdown", "curl"] # denylist always wins
# Allowed in general, denied for specific argument patterns (order-independent).
[[rules]]
command = "git"
deny_args = ["push --force", "push -f"]
reason = "force-push rewrites shared history"
[redaction]
enabled = true
entropy_fallback = false # noisy on hashes/UUIDs/base64 — see Caveats
extra_patterns = [] # e.g. [{ name = "internal_id", regex = "INT-[0-9]{8}" }]Honest caveats
This is a hardening layer, not a vault. Read these before deploying.
Redaction is best-effort, not a guarantee. It is regex/pattern-based. It catches common secret shapes (see the pattern list) but a novel or unusual format will pass through. Do not rely on it as your only secret control, and do not allowlist commands that print secrets you can't afford to leak.
The audit log is tamper-evident, not tamper-proof. The server writes append-only with per-record flush+fsync, so it won't lose records to a crash. But anyone with host filesystem access can read, edit, or delete
audit.jsonl. Protect it with OS permissions and ship it off-host if you need integrity.The jail is a path check, not a kernel sandbox. Containment is a resolve +
is_relative_tocheck on the cwd. There are no namespaces, cgroups, or seccomp. A process that can escape via a kernel bug or an allowlisted escape hatch is not contained. For stronger isolation, run the whole server inside a container/VM.Policy strength is entirely the operator's allowlist. The engine faithfully enforces what you configure. Allow
bash,python,env,find, or any tool with-exec/eval semantics and you have handed over a general-purpose shell. Keep the allowlist minimal and argument-scoped.
Testing
uv run pytest84 tests, isolated with tmp_path (they never touch a real policy or audit log):
test_policy.py(22) — deny-by-default, denylist-wins, case/spacing bypasses, metacharacter rejection, argument rulestest_executor.py(7) — shell=False, timeout+kill, output cap, env scrub, cwd validation, refused commandstest_server.py(16) — one test perResultCode, both tools, envelope shape, cwd escapes, explain-never-executestest_audit.py(15) — attempt/outcome records, fail-closed vs fail-open, schema, no output content, redactedcommand_raw/argv, jail-root rulestest_redact.py(23) — one case per pattern, no-false-positives (paths, SHAs, UUIDs,cp -p), entropy on/off, redaction before truncationtest_smoke.py(1) — import/health smoke
CI runs the suite plus gitleaks on every
push and pull request (see .github/workflows/ci.yml).
Related work
Asaad-Suliman/MCP-file-organizer — a companion safe-MCP-server project applying the same deny-by-default, audited, least-privilege approach to filesystem operations.
Asaad-Suliman/safe-mcp-suite — this server's successor: the terminal server and the file organizer rebuilt together on one shared, deny-by-default safety core.
License
MIT — see LICENSE.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Tools
Related MCP Servers
- AlicenseBqualityBmaintenanceA secure MCP server for Windows Subsystem for Linux environments, facilitating safe command execution with extensive validation and protection against vulnerabilities like shell injection and dangerous commands.710220MIT
- AlicenseBqualityDmaintenanceA secure MCP server for executing whitelisted shell commands with resource and timeout controls, designed for integration with Claude and other MCP-compatible LLMs.203897MIT
- Flicense-qualityCmaintenanceA secure, controlled terminal MCP server that enables executing whitelisted shell commands safely with multiple security layers.
- FlicenseAqualityCmaintenanceA minimal MCP server that lets AI hosts execute shell commands in a sandboxed workspace directory.1
Related MCP Connectors
Security scanner for MCP servers. Detect vulnerabilities, prompt injection, and tool poisoning.
An MCP server for deep research or task groups
MCP server for Blockscout
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Asaad-Suliman/hardened-terminal-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server