Context-specific aliases with fish shell

06/12/2020

We're using Docker for everything now, which is great but it's a big ask of some of our frontend devs who aren't as comfortable with a CLI as others. It's all very well and good to say "OK instead of yarn watch, you now need to run docker-compose exec app yarn watch"; someone comfortable with a CLI is going to get that. But for someone who already feels like every word they type might do something horrible, it's a harder sell. Yes, we have READMEs and that goes a long way, but at a busy agency you might work on 3 or 4 different projects a day, with wildly varying toolchains. Every time you have to open a README to know what you're doing is time lost, and a cognitive burden.

I toyed with using Makefiles for this sort of thing, and it works great for closed-ended scripts like yarn dev, but it's unworkable for yarn add $dependency, as you need to be able to pass dynamic arguments. And, again, docker-compose exec app yarn add dependency is an unreasonable expectation for someone who doesn't love the CLI - yarn add dependency is probably already nearly too much.

In comes fish shell's callback structure; a very simple notion that allows you to watch a variable, then fire a callback function when that variable changes. We can use this so that every time you cd, your $PWD variable changes and we fire a callback to see if there's a config file in the current directory. With that in mind, we can write a simple function that sources a file it finds, then you can write config files in fish's scripting language to do anything you want. We're going to use it to alias commands we're used to, to commands that are a bit more difficult.

Then in your project directory you can define your own aliases, and they'll get sourced when you cd to that directory.

This approach is great, and works really nicely. You can commit these files and share them with your collaborators so that everyone using fish benefits from the same aliases as long as they're injecting .fish_config in the same way that you are.

But! It does give a potential attack vector. Installing yarn is really easy: curl -o- -L https://yarnpkg.com/install.sh | bash, but actually it's a little too easy. If I was the nefarious type I might make a really nice and useful bit of code, and maybe to install it I give you a nice easy script like the above, but when you run this, I put a .fish_config in every directory on your computer that runs my code every time you cd to a new directory. I could do whatever I want - could recursively search for passwords from the current directory then send them to myself. This attack wouldn't need to be active for very long for me to do some pretty mean stuff to you and/or your clients. Hell if I was a real piece of work, I could inject code into your project, commit it silently and then that ends up in production if you have CI or your peer review isn't bullet-proof (spoiler: it isn't).

One thing we (should) know is that you can never trust the client. This is a pretty interesting angle because you are the client here. And you could be executing code you don't know about by sheer virtue of moving to a new directory. We can't have that.

There are a couple of potential issues here, and we can be over-paranoid if we want to. It depends how selective you are when you run things on your computer, and how much you trust your collaborators on projects.

  1. Consider the likelihood of a risk. In a perfect world, we can fully trust all software we install, but literally anything you run on your computer could exploit this if it felt like it. However, any software you run on your computer could also set this up transparently without you realising. So if you're worried that this gives potential attackers an opportunity, you should probably also be more thoughtful when you install things! This ties up a malicious attacker, in my opinion. If an attacker could set up a thing you're doing then use it to attack you, you've already lost.
  2. Someone on your project makes a typo that deletes all your files. What if you have a cachebusting command on your project that makes things run a little faster but someone wants an easy command to run, so they set up an alias for rm -Rf . / cache and you run it without realising and delete everything on your hard drive. Don't forget, sourcing a file executes it. If I don't get what I'm doing and wrap all my code in functions, you're just executing things your teammates write without knowing! Again, unlikely, but not impossible. Fortunately, explicit trust is relatively easy to write with text files so let's do it.

And there we have it. There's a little more setup, but it's a lot more robust than the initial approach. If you're collaborating, you can't trust others to not make mistakes. You can't even trust yourself but I can't solve that with a fish config. This will make running commands against your docker containers a lot easier, but it also has tonnes of other applications if you live in the command line like I do.