# simonmysun/ell is a Bash LLM client whose two permission checks quietly stop working under Git Bash

> ell drives OpenAI and Gemini style APIs from a Bash script, reads prompts from stdin, and can capture terminal context for follow-up questions. Its config permission check and its temporary auth header chmod are enforced by a filesystem that Git Bash cannot provide, so the same install behaves differently across WSL, MSYS2, Cygwin and Git Bash.

**simonmysun/ell** — A command-line interface for LLMs written in Bash.

- Repository: https://github.com/simonmysun/ell
- Stars: 430 · Forks: 20
- Language: Shell
- License: MIT
- Published: 2026-09-15 · Updated: 2026-09-15 · Language: en
- Canonical page: https://hysenlabs.com/projects/simonmysun-ell

## Link ell, not ell.sh, or the helpers go missing

The recommended install clones the repository shallow into an XDG data directory and puts it on your PATH permanently:

```bash
git clone --depth 1 https://github.com/simonmysun/ell.git \
  "${XDG_DATA_HOME:-$HOME/.local/share}/ell"
echo 'export PATH="${XDG_DATA_HOME:-$HOME/.local/share}/ell:$PATH"' >> ~/.bashrc
```

The location is a convention, not a requirement: you may clone anywhere, and only the directory on your PATH matters. The alternative is to leave the clone off PATH and link the launcher into a directory already on it:

```bash
mkdir -p ~/.local/bin
ln -s "${XDG_DATA_HOME:-$HOME/.local/share}/ell/ell" ~/.local/bin/ell
```

That second line is where the layout becomes visible. Two entry points exist at the top level, ell and ell.sh, and the instruction is explicit that the ell launcher is the one to link, not ell.sh. The reason is that ell resolves the symlink back to the clone, so the bundled helpers, templates and plugins are always found relative to the real directory. Link the wrong file and the surrounding directories stop resolving.

The repository top level shows what has to be found: helpers/, llm_backends/, plugins/, templates/, plus tests/, docs/, a CHANGELOG.md and a LICENSE. The ell script is thin by design and everything with substance lives one level down.

## Git Bash cannot hold the permission bits ell refuses to run without

Two security behaviours are named explicitly as unenforceable on Windows under Git Bash. The first is inside load_config, which refuses to source a world-writable .ellrc file. The second is a chmod 600 applied to a temporary file holding the auth header. Both checks depend on creating files whose group and world write bits can be cleared, and over NTFS Git Bash cannot create genuinely group/world-writable files in the first place, so there is nothing for the check to reject and nothing for the chmod to remove.

The consequences are described as security limitations rather than bugs to be filed, and the guidance is to use MSYS2 or WSL instead, which provide the safer alternatives. The middle ground is MSYS2 and Cygwin, which offer a fairly complete POSIX layer with ACL-backed file permissions, real symbolic links and script(1), so the whole feature set works there. WSL behaves like Linux and everything works. Git Bash is called intentionally minimal, and it also lacks script(1), which removes record mode, plus real symlinks by default.

For the most complete experience on Windows the preference is WSL or MSYS2 over Git Bash. The details live in the Windows section of docs/Configuration.md.

On Windows the ell command is a plain wrapper script rather than a symbolic link, so it still works when git does not create symlinks on checkout, which is the default there unless Developer Mode or administrator rights are available. No extra setup is needed: clone the repository, add its directory to PATH, and invoke it as usual.

## The XDG config and the legacy .ellrc are both live at once

Two configuration locations are in play and neither has been retired. The current path is an INI style file at ${XDG_CONFIG_HOME:-$HOME/.config}/ell/config, and the legacy ~/.ellrc is still read as well. Templates and plugins under the old ~/.ellrc.d directory are still picked up, so an install made before the move keeps working and no migration step is required.

That compatibility promise has a cost: with two locations live, the file actually in force depends on a precedence rule, and that rule is not in the README. It is delegated to the architecture document, which covers the startup sequence and configuration precedence along with the request pipeline and its four hook stages, backends, and record mode. If you are upgrading an old install, read that page before assuming which of your two files won.

A configuration is a handful of environment style assignments. Switching between providers means swapping five values, not switching backends in code:

```ini
ELL_API_STYLE=gemini
ELL_LLM_MODEL=gemini-1.5-flash
ELL_TEMPLATE=default-gemini
ELL_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ELL_API_URL=https://generativelanguage.googleapis.com/v1beta/models/
```

The same shape covers OpenAI, with ELL_API_STYLE=openai, ELL_LLM_MODEL=gpt-4o-mini, ELL_TEMPLATE=default-openai, a sk- prefixed key, and ELL_API_URL pointing at https://api.openai.com/v1/chat/completions. The style also has a command line equivalent, since the backend is selected with --api-style as well as with the variable. The template name and the style name usually match, which makes a typo in either one look like a provider outage.

## stdin is the input channel, so ell composes with pipes

The prompt can be an argument, a file, or a stream, and the file form accepts a dash for standard input:

```bash
ell "What is the capital of France?"
ell -m gpt-4o -f user_prompt.txt
cat somecode.py | ell -f -
```

Because the third form reads a pipe, extra instructions can be appended inline rather than baked into a template:

```bash
(cat somecode.py; echo "Explain this code") | ell -f -
```

That is the difference between a template change and a shell construct. The model is chosen per invocation with -m, so one install serves several models without rewriting config, and the same config file still supplies the key, the style and the URL.

Record mode is the other half. With -r, ell records terminal input and output and then uses it as context, so a session becomes a conversation about whatever just went wrong:

```bash
ell -r
# do random stuff
ell What does the error code mean?
ell How to fix it?
```

The dependency for that mode is util-linux, and specifically the script command, which is why it is the only entry in the requirements list marked optional. Without it, record mode is unavailable. The recorded example in the file is a capture the flag game, run as ell -r -i -t ctf-gemini or ell -r -i -t ctf-openai depending on the API in use, with the template selecting the side.

Interactive mode is plain ell -i, and turning it on automatically enables record mode so the conversation has context to work with.

## Provider function calling is delivered through templates

The extension story is split in two, and the split is easy to misread. A plugin, as ell uses the word, is a script that ell can call, used to extend its functionality. Provider side plugins, meaning the function calling support the LLM providers themselves offer, are not included in that definition and are handled elsewhere.

Function calling and other provider features arrive through templates instead. The template is chosen with -t and defaults from ELL_TEMPLATE, and the shipped names follow the backend: default-gemini, default-openai, ctf-gemini, ctf-openai. docs/Templates.md is the reference, and docs/Plugins.md covers the script kind.

Backends are the third layer and the one that decides the wire format. A backend adapts ell to an LLM API style, selected with --api-style or ELL_API_STYLE. OpenAI and Gemini work out of the box and you can add your own, which is the extension point for an API the two shipped styles do not match. Styling has its own page in docs/Styling.md, and the pipeline that ties these together is described in docs/Architecture.md.

One more behaviour sits outside all of that. Sensitive information redaction is called out as a feature and is tracked as issue 14, which places it in the same list as pipe support and chat rather than in the plugin system.

## Requirements, release gaps, and a Q&A that stops mid question

The dependency list is short and one entry is conditional: bash-4.1 or later, coreutils or the OS X equivalents, awk and sed, curl for sending HTTPS requests, and util-linux, which is not necessary unless you use record mode.

Release numbering has moved once in a long while. The tags visible are v0.1.0 and v0.1.1, both dated 2024-08-02, and v0.2.0 dated 2026-07-26, with the branch itself last pushed on 2026-09-26. So the project is on its second minor release after two years of patch level work, and the install instructions above clone the default branch rather than a tag, which means an ell on your PATH is whatever main currently holds.

The file also explains the name, which is worth knowing because it predicts the conflicts you will hit. ell is a combination of shell and LLM, chosen as a short and memorable word that does not conflict with any active software. shellm was considered and dropped because it could be misread, and the name cannot be shortened to L because that would conflict with too many things. The letter also collides with the standard error stream name, which is why the wrapper matters on a shell that redirects stderr.

The final section of the file is a question and answer pair that is cut off mid question, so whatever it was going to ask is not visible in the retrieved copy. The project has a CHANGELOG.md and CONTRIBUTING.md at the top level for anything else.

## A whole page is devoted to risks, for a tool that reads pipes and scrollback

Among the documentation links there is one that stands apart: docs/Risk_Consideration.md, given its own section rather than folded into configuration or usage. What it contains is not visible in the retrieved copy, so the risk surfaces have to be read off the tool's own behaviour.

There are four. The config file holds an API key in plain INI, in a per user directory that ell itself will refuse to read if it is world writable, which tells you the project treats that file as sensitive. The auth header is written to a temporary file and chmod 600 is applied to it, so the credential is expected to touch disk in passing. Record mode runs the script command to capture terminal input and output, meaning a whole session of what you typed can be forwarded as context. And because ell reads standard input, whatever a pipe hands it goes to the provider, which is the point of the tool and also the reason to check what sits upstream of a pipe.

Sensitive information redaction is listed among the features and tracked as issue 14, so the project is aware of the third of these. Whether it is on by default, and what it matches, is answered on the risk page rather than in the feature list.

The permission story ties the four together, because the mitigation for the second item depends on a filesystem that Git Bash does not provide. On such a host the key file and the temporary auth file both exist with weaker protection than the code assumes, which is the concrete reason the recommendation is WSL or MSYS2 rather than a preference.

## Conclusion

ell suits someone who already works in a Bash environment and wants LLM calls to be pipeable rather than a separate chat client, and its stdin and record modes are the reason to reach for it. Install it under WSL or MSYS2, where the filesystem can actually hold the permission bits the script checks for, and treat Git Bash as a degraded mode that runs but cannot enforce them. Before pointing it at a real key, confirm which config file is being read, since the XDG path and the legacy .ellrc path are both live, and check the base URL you configured against the endpoint your API style expects.

## FAQ

### What does simonmysun/ell need installed to run?

bash-4.1 or later, coreutils or the OS X utilities, awk, sed, and curl for HTTPS requests. util-linux is needed only for record mode, because it provides the script command that records terminal input and output.

### Which config file does ell read, and is the old one still supported?

The current file is ${XDG_CONFIG_HOME:-$HOME/.config}/ell/config, and the legacy ~/.ellrc is still read along with templates and plugins under ~/.ellrc.d, so no migration is required for an older install.

### Why should I not link ell.sh when installing ell?

The ell launcher is the one to link, not ell.sh, because it resolves the symlink back to the clone so the bundled helpers, templates and plugins are found. Linking elsewhere leaves those directories unresolvable.

### What stops working for ell under Git Bash on Windows?

Git Bash cannot create genuinely group/world-writable files over NTFS, so the load_config check that refuses a world-writable .ellrc and the chmod 600 on the temporary auth-header file cannot be enforced. It also lacks script(1) for record mode, and WSL or MSYS2 are the recommended alternatives.

### How do I use ell with an OpenAI or Gemini model?

Set ELL_API_STYLE to openai or gemini, ELL_LLM_MODEL to the model name, ELL_TEMPLATE to the matching default template, plus ELL_API_KEY and ELL_API_URL. The style can also be chosen with --api-style, and a model can be overridden per run with -m.

### How does ell handle function calling from LLM providers?

Not through plugins. In ell a plugin is a script the tool can call, while provider plugin support such as function calling is delivered through templates, selected with -t or with ELL_TEMPLATE.

## Sources

- [Issues](https://github.com/simonmysun/ell/issues)
- [License: MIT](https://github.com/simonmysun/ell/blob/main/LICENSE)
- [README](https://github.com/simonmysun/ell/blob/main/README.md)
- [Releases](https://github.com/simonmysun/ell/releases)
- [simonmysun/ell on GitHub](https://github.com/simonmysun/ell)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/simonmysun-ell
