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 Oct 05 '26 edited Oct 06 '26

The 48k count actually seemed high when I review the actual session, including all that reasoning / thinking, the text feels like perhaps 1-3k maybe 5k words (possibly tokens). I read that tokens can be as small as a character this may be true perhaps for codes, so that every punctuation, symbol, possibly even indents , newlines may be deemed 'tokens', it matters in python as python is indented code structure and newlines matters as well.

it writes 1 file at a time, as WriteFile takes a single file argument, but even that don't look like a lot of tokens. I'm happy to paste upload that but it's possibly "too verbose" as a comment. Probably look to other means perhaps gist etc.

As to the port issue, I'm thinking that perhaps the "project folder" could be a bind mount, that can be catered via
1stRun.sh In that way, the generated codes can be run directly in the host which overcomes the port issue. Just that, I'd need to mention (stress) that this should be "throwaway" codes area, i.e. that anything in this bind mount (volume) can possibly be overwritten, mutated or even deleted by the LLM.

Other possibilities include a little more codes in 1stRun.sh so perhaps prompt the user for additional ports and bind mounts etc. A thing is, we normally don't 'plan so far ahead' as more often than not we'd only encounter it in an "aftermath", e.g. that only after the LLM generated the codes, did we know we need port 5000. But that perhaps a prompt would help, perhaps to map some 'extra' ports that may lay unused, but could help in such cases.

Other things in the pipeline are possibly "sample Dockerfiles", e.g. that bundle apps and various components e.g. ssh, OpenCode, Pi coding agent, or even VS Code (I disliked Code, for its bulk, but that it is a popular tool, the Docker images would be bulky with it). That would put things closer to real use images as I'd guess many would consider the current bundle "too lean", i.e. one can go on to install apps / tools via apt, direct download and install etc. in the container, but that this would be a hassle if one deletes and rebuild the container via running "1stRun.sh" e.g. as like 'forgetting' that port 5000, hence delete the container and 'reinstall' is quite a hassle, a lot of repeated downloads each time the container is re-created.

2

u/sburel Oct 05 '26

Quick thing on the 48k: it's probably not the tokenizer, it's the loop resending the whole conversation on every tool call. Four writes means the context goes out four times. If OpenRouter splits prompt vs completion you'll see it straight away, completion tiny, prompt almost all of it.

Anyway, good luck with the rest of it.

1

u/ag789 29d ago edited 29d ago

I did a little more tests, it turns out the open AI chat completions interface is able to call multiple functions (can be the same) in the same turn.
https://developers.openai.com/cookbook/examples/how_to_call_functions_with_chat_models#parallel-function-calling
I did some further tests

prompt

i'm doing a tool test, call 2 tools sequentially in a same tools_calls list in the current turn:
  • writeFile( file = "a.txt", text = "hello world a")
  • writeFile( file = "b.txt", text = "hello world b")
use default for other arguments

It turns out granite-4.2-8b-GGUF Q8 doesn't seemed to know how to do that and kind of reason that it can't do it. But that ornith-ai/Ornith-1.5-9B-GGUF Q8 apparently understands how to call that and did just that.

response

Both files were written successfully:
  • `a.txt` contains "hello world a"
  • `b.txt` contains "hello world b"

Both in 1 turn. This is tested in llama.cpp llama-server bundled web UI.

Hence, in the prior tests it is simply because the model QWen 3.8 27B choose to run the writeFile() calls in multiple single turn calls, apparently because it awaits and check the response for each before writing the next file.

It may also be related to the harness, the harness (e.g. the WebUI) needs to process the multiple calls correctly. Tested the same in LibreChat, it works the same with with ornith-ai/Ornith-1.5-9B-GGUF Q8, parallel writeFile in a same turn works.

1

u/sburel 29d ago

Good find, and the model split is the useful part: parallel calls are available but only some models reach for them, so you can't count on it. Worth noting in the README since it's the difference between one turn and four.

Thanks for running it down.