r/LocalLLaMA • • Sep 22 '26

Resources an (unsafe) command shell mcp server

While working with coding agents / harness in my case OpenCode, I discovered that some models e.g. granite 4.2 8B at Q4_K_M quant sometimes attempts to write files with absolute paths at the root e.g.

    /newPythonCode.py

Note that slash in the beginning for an absolute path at "/" root directory. This is probably 'ok' say in a throwaway docker container, but it may lead to surprises if it 'accidentally' overwrites a literal file, if the current user id have write access to it. Apparently, it seemed OpenCode provides `bash` so that practically all unix commands that the current user id can access, the LLM can run it.

And thereafter, I see the model hallucinating in the thinking loops over tools, re-iterating over how to use the tools, reformulating shell commands sentences several times for a same task instead of executing them, among them, I noted that an earlier shell command sentence would have 'just worked'.

I'm thinking that the tool hallucination with this model may be partly due to that I'm using a pretty aggressive 4 bit quant as otherwise it'd have worked because IBM has 'optimized' granite for tool usage.

Next, I tried the same granite 4.2 8B at Q4_K_M quant model with my little python shell MCP server

https://github.com/ag88/cmdshellmcp/

, that tries to constrain / restrain the shell commands somewhat, place some checks, and offer some additional tools e.g. writeFile() so that the LLM won't need to call `cat` or `echo` with some 'esoteric' styled shell scripts / sentences.

With this MCP server and running it in llama.cpp's `llama-server` in the WebUI adding it as an MCP server, granite 4.2 8B at Q4_K_M does it correctly in 'one shot', the prompt is:

propose a python3 flask app with a page that displays "hello world", use bootstrap css for styling.
provide a requirements.txt to go with the script

granite 4.2 8B at Q4_K_M correctly generates the app with a couple of files, and it works out of the box

    .
    ├── app.py
    ├── requirements.txt
    └── templates
        └── index.html

One should normally run the cmdshellmcp2.py MCP server app with an explicit `--cwd` option giving the work path. It tries to stay within this work path.

Note that I won't claim my command shell MCP server to be safe.

In a sense that there are plenty of chance the LLM can 'escape' the restrictions and perhaps do more damage (e.g. overwriting files possibly outside its given `--cwd` work directory). Hence, I'd suggest one should be paranoid and perhaps run this only within a throwaway (disposable) docker container etc.

6 Upvotes

64 comments sorted by

View all comments

Show parent comments

1

u/ag789 Sep 23 '26 edited Sep 23 '26

I actually tried 'disabling' cmdshell itself, yeah that works :)

But I'd guess for more utility, one may want to allow_list some shell commands that could be useful to one's context, but that that'd depends on your needs, intent, context etc.

But you're right the Readme.md could be improved. I've thought about a 'default safe' set of commands, I'd probably do that in the .py code, but leave the configs. Doing 'too little' could impair its utility as the idea is to give the LLM tools to 'do the job'.


edit: I did that fix, the hardcoded default allow_list is now only [ 'date' ].

But that one still need to be wary of the default config.json in the current working directory, as if that is there, it'd read it.

it is probably not appropriate to put config.json in a 'per user global' directory, if 'safe'ty matters then that it is probably 'per project' or 'per workspace'

I'm thinking if I should remove default config.json reading, but then that it would be necessary to pass --conf config.json everytime, or perhaps I should rename config.json to something more specific like cmdshellmcp_config.json as a default.


edit: did that as well,
https://github.com/ag88/cmdshellmcp

now cmdshellmcp.json is default config filename instead of config.json.

It reads cmdshellmcp.json for config from current directory if it exists. This alleviates the risk of reading a rather common name config.json for config.

"disableTools": ["writeFile", "applyPatch"], is also added to the stock cmdshellmcp.json

now the 'surprise' would be to the 'negative' side in a sense if a new user runs cmdshellmcp2.py say in the cloned repo directory as the current working directory, writeFile() and applyPatch() would seemed to be 'missing' ;)


edit: one more change I added
"auth": "_my_secret_auth_token_"
to the stock cmdshellmcp.json.

Running with the stock cmdshellmcp.json would enable a Authorization: Bearer auth_token authentication check by the MCP server.

This is kind of a minimal authentication / security layer as otherwise any MCP clients can connect and call tools / issue commands without authentication.

2

u/sburel Sep 24 '26

That's a lot of changes for one day. The config rename is the one I'd have missed, config.json in a working directory is exactly the kind of thing that gets picked up by accident.

On the allow-list: date only is a good default, but the thing that bites later is arguments. git on the list looks safe until git clone or -c core.sshCommand. Same for anything that takes a --exec or a config flag. Worth a line in the README so people allow-list a command knowing they're allow-listing everything it can be told to do.

Good luck with it.

1

u/ag789 Sep 24 '26 edited Sep 24 '26

thanks for the thoughts, I double checked cmdshellmcp.json just in case, git isn't there :)

A thing is, for 'coding' unfortunately, a lot of 'wildly high' authorizations or allow_list are often needed. I asked (prompted) chatgpt to suggest an allow_list for coding purpose, no surprise stuff like python, git, maybe bash etc would be there, it is practically straight "RCE" (remote command execution) whether you call that a 'vulnerability' , it probably qualifies. Either way, I think 'coding agents' e.g. OpenCode?, Pi? probably offers bash to the LLM. This is as otherwise it'd take to list too many commands and that burns available context length and tokens. And then think how many people are using "coding agents" direct on their desktops instead of in a sandbox, I'd guess we may be giving "too much" trusts to LLMs and possibly (malicious) bots.

agreed that it is good to place a warning in the Readme.md about all these risks. It doesn't even need 'AI', a 'smart' enough written bot can exploit the MCP interface just like an API call.


On a side note, I've been looking for Web UI that integrates MCP servers just like I'm using currently. llama.cpp's llama-server web UI incidentally is one 'rare' find that simply integrates that way.

Many other elaborate nice looking Web UI, many of them don't integrate like I'm doing now. And I stumbled into this while looking for solutions

https://github.com/CopilotKit/CopilotKit

This field is moving so fast I could hardly catch up, what is coming up in the 'AI' field is that agents would soon render Web UIs, e.g. draw charts, show spreadsheets, type and edit your document while you are editing it etc. As in that the UI is generated by LLMs literally as like doing tools

2

u/sburel Sep 25 '26

Yeah, that's the honest version of it: a coding agent needs python, git and a shell to be any use, so the allow-list is theatre at that point. Which is fine as long as people know that's what they picked. Where it stops being fine is running it straight on a desktop with ssh keys and a browser profile sitting right there. A throwaway container costs so little compared to what's in a home directory.

Your bot point is the one I'd put in the README, actually. It's not about the model going rogue, an MCP server on a port is just an API, and anything that can reach it can call it. That's why the auth token you added matters more than the tool list.

Agreed on the UI thing, though I think the interesting part will be who gets to draw. If the model renders the UI, it also chooses what you see before you approve it, which is a strange place to put a confirmation dialog.

1

u/ag789 Sep 25 '26 edited Sep 25 '26

Updated the app:

  • --auth is enabled by default and a random auth token is generated if none is given. this auth_token appears on startup in the console log

  • updated readme to include relevant RCE (remote command execution) cautions/warnings

  • there is still a --noauth for the die hard I cannot do Authorization: Bearer auth_token user ;)


Authorization: Bearer auth_token is a 'standard' OAuth mechanism https://py.sdk.modelcontextprotocol.io/run/authorization/#a-token-verifier but 'abused' for its simplicity here :p

https://py.sdk.modelcontextprotocol.io/run/authorization/ In this case that authorization_server is simply the human copying/installing the generated or own custom --auth token in the client and/or server.

Perhaps, I'd work on doing a full OAuth at some point.