Personal dotfiles / config backups (macOS and Linux).
git clone https://github.com/mbottoni/confs.git ~/configs
cd ~/configs
./install.shThat symlinks every config for the current OS into place. Anything it would
overwrite is moved to ~/.dotfiles-backup/<timestamp>/ first (same relative
path, so restoring is a mv away), and re-running is safe — links already
pointing at the repo are left alone.
Preview first with ./install.sh --dry-run.
| Flag | What it does |
|---|---|
--dry-run, -n |
Print what would happen, write nothing |
--copy |
Copy files instead of symlinking (edits then don't flow back to the repo) |
--only zsh,tmux |
Install just those modules |
--skip zotero |
Install everything except those |
--list, -l |
List module names |
--packages |
Also install the CLI tools the configs expect (brew / apt / dnf / pacman) |
--cursor-extensions |
Also reinstall the Cursor extensions from cursor/extensions.txt |
--help, -h |
Usage |
Modules: zsh, shell, bash, zprofile (macOS), nvim, tmux,
alacritty, lf, htop, mpv, yabai (macOS), claude, opencode,
ollama, cursor, zotero. macOS-only modules are skipped automatically on
Linux.
- Writes
export ZDOTDIR="$HOME/.config/zsh"to~/.zshenv—.zshrclives under~/.config/zsh, and zsh won't find it otherwise. - Creates
~/.cache/zshforHISTFILE. - Clones
zsh-autosuggestionsfrom upstream (the repo only has a placeholder). - Links
~/.tmux.conf→~/.config/tmux/tmux.conffor tmux < 3.1. - On Linux, writes
~/.tmux_local.conftranslating thepbcopyclipboard bindings towl-copy/xclip/xsel(skipped if the file already exists). - Picks the right Cursor config directory per OS.
exec zsh # or restart the terminal
nvim +PlugInstall +qall # fetch neovim plugins
chsh -s "$(command -v zsh)" # if zsh is not the login shell yet
bash .macos # macOS defaults — optional, changes a lotZotero prefs are not applied automatically, see below.
| Dir / file | What it is | Lives at |
|---|---|---|
zsh/ |
zsh config, p10k, plugins | ~/.config/zsh/ (needs ZDOTDIR) |
.zprofile |
login shell profile | ~/.zprofile |
.bashrc |
bash config | ~/.bashrc |
aliasrc, shortcutrc |
shell aliases/shortcuts | sourced from ~/.zshrc |
nvim/ |
Neovim config (see below) | ~/.config/nvim/ |
alacritty/ |
Alacritty terminal | ~/.config/alacritty/ |
tmux/ |
tmux | ~/.config/tmux/tmux.conf (+ ~/.tmux.conf link) |
lf/, htop/, mpv/, yabai/ |
misc tools | ~/.config/<tool>/ |
.macos |
macOS defaults script | run once |
vscode/ |
VS Code profile export | import via VS Code profiles |
cursor/ |
Cursor editor | see below |
claude/ |
Claude Code | see below |
opencode/ |
opencode (local-LLM agent) | ~/.config/opencode/ |
llamacpp/ |
llama-server launch + agent | see below |
ollama/ |
Ollama model definition | fallback, see below |
zotero/ |
Zotero | see below |
nvim/init.vim -> ~/.config/nvim/init.vim
nvim/autoload/plug.vim -> ~/.config/nvim/autoload/plug.vim (vim-plug itself)
Plugins are not tracked — init.vim declares them and vim-plug fetches them
into ~/.local/share/nvim/plugged/ on :PlugInstall. :PlugClean removes
whatever is no longer declared.
Note there is also a stale ~/.config/nvim/plugged/ on older machines: that was
the install path of an earlier vim-plug. The current one uses
stdpath('data'), so anything under ~/.config/nvim/plugged is dead weight and
can be deleted.
Completion is Copilot only — coc.nvim was removed, along with supertab,
completion-nvim and deoplete-jedi (three things competing for <Tab>).
Linting is ALE. vim-pydocstring needs doq, which its make install hook
installs on first :PlugInstall.
cursor/settings.json -> ~/Library/Application Support/Cursor/User/settings.json
cursor/keybindings.json -> ~/Library/Application Support/Cursor/User/keybindings.json
cursor/extensions.txt # installed extensions
On Linux the User dir is ~/.config/Cursor/User/ instead.
Reinstall extensions (./install.sh --cursor-extensions does this for you):
xargs -n1 cursor --install-extension < cursor/extensions.txtNot tracked: ~/.cursor/ (extensions, plans, projects, chat state, argv.json)
— machine-local state, regenerated by Cursor.
claude/settings.json -> ~/.claude/settings.json
Plugins and marketplaces are declared inside settings.json
(enabledPlugins / extraKnownMarketplaces), so Claude Code reinstalls them
on first run. The rest of ~/.claude/ (sessions, history, projects, caches) is
local state and deliberately not tracked.
Runs opencode against a local model, no API key and no
network. Sized for a 16 GB M1 Pro. The runtime is llama-server from
llama.cpp; Ollama is kept installed as a
fallback but is no longer what opencode talks to.
opencode/opencode.json -> ~/.config/opencode/opencode.json
llamacpp/llama-server.sh -> ~/.config/llamacpp/llama-server.sh
llamacpp/com.maruan.llama-server.plist -> ~/Library/LaunchAgents/ (rendered, not linked)
ollama/Modelfile.coder-q3 -> ~/.config/ollama/Modelfile.coder-q3 (fallback)
Setup on a new machine:
brew install llama.cpp
mkdir -p ~/models/gguf
curl -L -o ~/models/gguf/Qwen3.5-9B-UD-Q4_K_XL.gguf \
https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-UD-Q4_K_XL.gguf
./install.sh --only llamacpp,opencodeinstall.sh renders the LaunchAgent and bootstraps it, so the server comes up on
login and stays up. Then opencode in any project picks the local model up from
the config. aliasrc defines llama-restart, llama-log and llama-health for
the server, and oc for opencode.
Ollama runs llama.cpp underneath, so this is not a speed argument — it is about one flag. Check what Ollama actually launches:
ps aux | grep llama-server
# ... --no-jinja --chat-template chatml ...--no-jinja discards the chat template baked into the model and substitutes a
generic ChatML one. That template is what defines the <tool_call> wrapper, and
llama.cpp selects its per-model tool-call parser from it. Without it there is
nothing to parse tool calls out of, so they come back as prose in the message
body and opencode never executes them.
That is the whole reason Qwen2.5-Coder "did not support tools" here, which the
previous version of this README recorded as a property of the model. It was not
— it was the runtime throwing the template away. Running llama-server directly
with --jinja (the default in current builds) returns a structured tool_calls
field.
The same flag removes the other hack: Modelfile.coder-q3 had to inline a
hand-edited copy of Qwen's template to force thinking off, because Ollama's
think: false only works on /api/chat and is ignored on the OpenAI-compatible
/v1 endpoint opencode uses. llama.cpp has --reasoning off, which drives the
template's own switch. No forked template to maintain.
Qwen3.5-9B at UD-Q4_K_XL — 5.97 GB of weights, ~8.5 GB resident with a 32k
context. It replaces Qwen3 8B: same size class, but tool-calling and coding were
specifically improved in the 3.5 release, and it does not need the template
surgery.
The 30B-class models that are genuinely strong at agentic coding (Qwen3-Coder-30B-A3B, Devstral-24B) need ~18 GB at Q4. Q3 fits on paper at ~13 GB but leaves nothing for KV cache or macOS and swaps. 9B dense is the ceiling on this machine — that is the binding constraint, not the config.
llamacpp/llama-server.sh explains every flag inline.
aliasrc defines oc as opencode --auto, which approves every permission
that is not explicitly denied — no prompting. Plain opencode still asks before
editing, since opencode.json sets permission.edit to ask. Note that in
headless opencode run without --auto, that ask silently skips every
edit — exit 0, file unchanged, no error.
Three things dominate latency, all fixed here:
- The model unloading when idle. Under Ollama this meant reloading 5 GB on
the next prompt after 5 minutes, and the fix (
OLLAMA_KEEP_ALIVE=2hin Homebrew's plist) got overwritten every time the service restarted or upgraded.llama-serverholds the model for as long as it runs, and the LaunchAgent undercom.maruanis hand-written, so nothing regenerates it. - Tool definitions were 54% of the prompt — 5,297 tokens of the ~7,750 sent
every step, against 2,456 for the actual system prompt.
opencode.jsondisablestask,skill,todowriteandwebfetch, cutting the prompt 30% to ~5,400 tokens. Re-enabletodowritefirst if you want one back; it is the cheapest of the four. - Reprocessing the prompt when its middle changes. Agent prompts shift in
the middle, not at the end — a file listing or timestamp moves and everything
after it does too, which defeats a plain prefix cache.
--cache-reuse 256lets the server KV-shift around the changed chunk.
Quantising the KV cache (q8_0 vs f16) makes no measurable difference to
speed — it is kept only because it saves 2.2 GB.
Do not leave Ollama running alongside it. With OLLAMA_KEEP_ALIVE=2h its
server holds ~8 GB for two hours after any use, and 8 GB plus llama.cpp's 8.5 GB
does not fit in 16 GB — the machine swaps and both get slow. brew services stop ollama; start it again only when actually falling back.
Against the previous Ollama + Qwen3 8B setup, same machine, same opencode config:
| Ollama / Qwen3 8B | llama.cpp / Qwen3.5 9B | |
|---|---|---|
| prompt processing | ~111 tok/s | ~209 tok/s |
| generation | ~24 tok/s | ~19 tok/s |
| cold ~4.4k-token prompt | — | 21 s |
| warm follow-up turn | ~3 s | ~4.5 s |
| correct one-function edits | 3 / 8 | 8 / 8 |
| thinking tokens emitted | 0 (forked template) | 0 (--reasoning off) |
Generation is slightly slower because this is a 9B at a fatter quant, not an 8B
at Q4_K_M. Prompt processing — which is most of the wait, since every agent
turn re-sends a ~5.4k-token prompt — nearly doubles.
The edit corruption that dominated the Ollama setup is gone. 8 of 8 runs of the same one-function edit produced code that parsed and behaved correctly, against 3 of 8 before. The specific failures that are no longer reproducible:
- Literal
\nwritten into the file, from the model double-escaping newlines in tool arguments. - Indentation flattened, giving an
IndentationError. - No edit at all: reads the file, answers, changes nothing.
Most of that was never a sampling problem — it was the generic ChatML template mangling tool arguments on the way out. Lowering the temperature only ever masked it.
What has not changed:
- This is still a 9B model. It is good at single-file, well-specified edits and gets vaguer the larger the task. It is not a substitute for a frontier model on anything architectural.
opencode --auto(aliased tooc) approves everything, including the loop detector. That is the tradeoff being made deliberately — run it in a git repo and read every diff.
Reads and questions are reliable. Treat edits as a draft to review, not as work that is done.
llama-health # {"status":"ok"}
llama-log # tail the server log
llama-restart # after editing llama-server.shIf tool calls come back as prose in the message body rather than as a
tool_calls field, the template is not being used — check that --jinja is
still in effect and that opencode is pointed at port 8080, not at Ollama.
zotero/prefs.js # curated prefs, append to the profile's prefs.js
zotero/plugins.txt # installed plugins (reinstall from their releases)
zotero/styles/nature.csl -> ~/Zotero/styles/
Prefs live in ~/Library/Application Support/Zotero/Profiles/<profile>.default/prefs.js.
Quit Zotero before editing it, or drop the lines into a user.js next to it.
The library itself (~/Zotero/zotero.sqlite, storage/) is not tracked — that
is data, handled by Zotero sync. Credentials/API keys are excluded from
prefs.js on purpose.