Oh no! This site works best on Firefox. It looks like you are using a Chromium-based browser (Chrome, Edge, Opera, etc.) 😭
To help save the open web and protect your privacy, you should give Firefox a try!

Secure Project-specific (Neo)vim Config


Update: exrc is now secure in neovim

This PR adds a prompt to neovim 0.9 before loading a local .nvimrc file, when exrc is enabled.

If you are using regular vim or stuck on an older neovim version, this post might still be interesting for you. The rest of this post is unchanged.


Why do you want per-project vim configs?

Usually, I have all my vim configurations stored in my dotfiles. These global configs are easy, as they don't change too often, and are applicable for all my personal projects. So when they do change, I do a git pull on all my devices and be done.

However, I don't only contribute to my own projects but also to other peoples' projects. So from time to time it happens, unfortunately, that some projects' style guides disagree with certain things in my config.

Up to now, I used to have set exrc in my init.vim file, for these cases. This setting enables reading of local .nvimrc config files in the current directory. The major downside of this solution is that it reads and executes everything from (1) a hidden file (2) on editor startup. That is a nasty combination that only waits for a desaster to happen. If I carelessly clone a malicious repository and open vim in one of its directories, it is already too late.

Why is set secure not enough?

There are a lot of stackoverflow answers and blog posts recommending set exrc as a solution to the per-project configuration. Usually they recommend to set the 'secure' setting additionally, to make it a safe solution. Unfortunately, this is not sufficient. The neovim help has the following to say:

Image of vim's :help secure output in a terminal window.

Note that there is a pretty strong restriction on the usefulness of this setting hidden in this sentence. Specifically:

On Unix this option is only used if the ".nvimrc" or ".exrc" is not owned by you.

This does not cover our threat model of a malicious git repository, at all! When cloning a repository, we own all files in there, of course, so this setting does absolutely nothing.

Neovim has decided to deprecate the exrc option. So for neovim users the exrc setting will not even work anymore in the future.

Solution

Since I use direnv on many projects anyways, I decided to make it manage my per-project vim configs, too. This is convenient, because direnv needs to solve the security implications of loading random files into the shell anyways, so it should solve the 'malicious git repo' use case automatically. And to nobody's surprise, it does:

In some target folder, create an .envrc file and add some export(1) and unset(1) directives in it.
On the next prompt you will notice that direnv complains about the .envrc being blocked. This is the security mechanism to avoid loading new files automatically. Otherwise any git repo that you pull, or tar archive that you unpack, would be able to wipe your hard drive once you cd into it.
So here we are pretty sure that it won’t do anything bad. Type direnv allow . and watch direnv loading your new environment

So to make vim load my project's .init.vim file, I simply added the following .envrc to my projects:

export VIMINIT='source $MYVIMRC'
export MYVIMRC=$PWD/.init.vim

This loads the custom .init.vim whenever I open vim in the project's directory. However, it will only be loaded if I specifically ran direnv allow . in that project. This is a sufficient safeguard against malicious repos, since it is not fully automatic. I can decide if I want to load other people's .envrc files at all, and if I do, I carefully review all .env/.envrc files in the project.
After allowing it once, direnv will load the variables automatically, until the .envrc file changes or is specifically disallowed.

To extend my global init.vim, I source it and overwrite only the project-specific settings:

so ~/dotfiles/init.vim
set tabstop=2
set shiftwidth=2
set expandtab

I hope this helps. If someone has an easier method, please let me know.

Do you have any questions or thoughts on this post? Send me a message!