Untrusted projects
If a project has a plugins/ folder and you have not trusted it, hexiel skips the files and prints one line saying how many it skipped and how to trust the project.
Nothing in them is imported.
Free & open source
A plugin is a plain Python file. Drop it in the plugins folder and the methods you wrote show up as tools the model can call. hexiel reads the same Tools, Pipe and Filter files that OpenWebUI uses, so community plugins work as they are and the ones you write work in both.
mkdir -p ~/.local/share/hexiel/plugins
cp weather.py ~/.local/share/hexiel/plugins/ # any OpenWebUI "Tools" file
hexiel # restart: plugins are read at startupAsk hexiel something the plugin can answer. Every public method of its Tools class is now a tool named
tools_<method>, for example tools_get_weather. Plugin tools always ask for your approval before they run, because hexiel cannot know what a
plugin touches.
Plugins are loaded once, when hexiel starts. After adding or editing one, restart hexiel.
| Folder | Loads |
|---|---|
~/.local/share/hexiel/plugins/ | Always. This is your own folder, so it is the place for plugins you chose to install.
It follows HEXIEL_DATA if you set it. |
plugins/ inside a project | Only after you trust that project (see below), so code in a repository you just cloned never runs by surprise. |
Each *.py file in these folders is imported. A file can hold a Tools, a Pipe and a Filter class together.
A plugin is code that runs on your machine with your permissions, the moment hexiel starts. Treat it like any program you install: read it, or take it from someone you trust.
cd my-project
hexiel --trust-project # remember this project; its plugins/ folder now loadsIf a project has a plugins/ folder and you have not trusted it, hexiel skips the files and prints one line saying how many it skipped and how to trust the project.
Nothing in them is imported.
Even a trusted plugin's tools go through the normal approval prompt. A Pipe only forwards text to a model, so it is the one kind that runs without asking.
The list of trusted projects is a small file, trusted-projects.json, in the hexiel data folder. Delete a line to un-trust a project. Setting
HEXIEL_TRUST_PROJECT=1 trusts every project for that run, which is meant for throwaway CI containers.
Save this as ~/.local/share/hexiel/plugins/notes.py:
from pathlib import Path
class Tools:
"""Search my notes folder."""
class Valves: # optional settings, with their defaults
notes_dir: str = str(Path.home() / "notes")
max_hits: int = 5
def search_notes(self, query: str, limit: int = 3) -> str:
"""Find notes that mention a word or phrase.
Returns the file name and matching line for each hit."""
root = Path(self.valves.notes_dir)
hits = []
for path in sorted(root.glob("**/*.md")):
for line in path.read_text(errors="replace").splitlines():
if query.lower() in line.lower():
hits.append(f"{path.name}: {line.strip()}")
return "\n".join(hits[: min(limit, self.valves.max_hits)]) or "no matches"Restart hexiel and it can call tools_search_notes. How hexiel reads your file:
| In your file | What hexiel does with it |
|---|---|
A class named Tools | Every method whose name does not start with _ becomes a tool, named tools_<method>.
startup, shutdown and valves are skipped. |
| The method's docstring | Its first paragraph is the description the model reads when it decides whether to call the tool. Write it for the model: say what the tool does and returns. |
| Type hints | int, float and bool become numbers and booleans, list and dict become arrays and objects, and anything else is text. |
| Defaults | A parameter with a default is optional. One without a default is required, and the model must supply it. |
A Valves class | hexiel builds it with its defaults and sets it as self.valves, so your methods can read their settings. |
| The return value | Text is passed back as is. A dict or list is turned into indented JSON. async def methods work too. |
Keep each tool small and give its text result a sensible size. The result goes into the model's context, and a short, relevant answer saves tokens and money.
A class named Pipe with a pipe(self, body, ...) method. hexiel offers it as one tool named pipe_<id> (the class's id attribute),
passing the conversation messages in body["messages"]. Use it to forward a question to another model or service.
A class named Filter with an inlet(self, body, ...) method. It sees each message you send, before the model does, and can return the
changed text in body["messages"][-1]["content"]. Only inlet is applied today; outlet is not.
class Filter:
def inlet(self, body: dict, __user__=None) -> dict:
body["messages"][-1]["content"] += "\n(Answer in British English.)"
return bodyIf a filter raises an error, hexiel shows a short note and sends your message unchanged.
The compatibility also works the other way. In OpenWebUI add a connection to http://127.0.0.1:8777/v1/chat/completions while hexiel runs with
hexiel --serve. Over that API hexiel keeps its read-only tools and turns its write tools off.
They are in a project's plugins/ folder and the project is not trusted. If you trust it, run hexiel --trust-project in that folder.
Otherwise copy the file into ~/.local/share/hexiel/plugins/.
Restart hexiel, since plugins load at startup. Check the class is named exactly Tools (capital T), the method name does not start with an underscore,
and the file ends in .py.
The file raised an error when imported, usually a missing package. Install it into the Python that runs hexiel. A broken plugin never stops hexiel from starting; the rest still load.
Improve the docstring's first paragraph. It is the only thing the model reads to decide. Say what the tool is for, in plain words, and what it returns.
That is by design for plugin tools. Choose "always" at the prompt to allow that kind of call for the session.
Still stuck? Run hexiel --doctor, or open an issue.