Things in monospace


Using 1Password CLI with Magit Forge

Authored on 2025-11-26

I've been trying to move as many of my code review processes towards Emacs, relying mostly on gh CLI tool. But as a user of magit it has always irked me that I have to hop into kitty session just to get an idea of what issues are there, are there any linked PR, what's the CI status. Of course, I could do it in shell mode within Emacs but I have to say that never felt pleasant as Emacs is just not a good terminal emulator.

So I started looking into what I can achieve with Magit and ended up writing 1 (one) line of ELisp and this article.

The Forge

The terminology of "forge" is fairly foreign to me. Nevertheless, it seems to be the accepted way of describing the various source control hosts i.e. GitHub, GitLab, Codeberg, SourceHut and many, many more.

Magit helpfully describes the authentication setup in their docs. In essence it boils down to querying auth-source for a tuple of (host login) where host is api.github.com/api.gitlab.com/etc and the login is a special string in the form of "username^forge".

Which is what I have done, but setting up a new entry, calling it api.github.com and providing a single entry called mishok13^forge. And... I got a 401 from GitHub.

Now first thing first is validating the token which I promptly did just to discover it worked. Next step was issue a new GitHub token just to be on the safe side, running M-x auth-source-forget-all-cached and... no change, still 401.

This is where I decided to take a closer look at Magit Forge itself.

How Magit Forge authenticates

The codebase is clean and easy to follow, so I thought it would be a simple case of search and yet this is where I hit my first hurdle. auth-source is mentioned only in docs of Forge and not in the code. After getting a little bit panicked, turning on emacs-debug and reading tracebacks a little bit more carefully (i.e. reading them for the first time) I have discovered that the authentication logic lives in a separate package, ghub.el. I'm yet to understand why it's not part of forge itself but I would speculate it's simply historical reasons. The implementation for username rendition can be found here in all its glory https://github.com/magit/ghub/blob/b12183279be880dafc6d971aa4bc1ccc39350c4a/lisp/ghub.el#L781-L782

With this information in mind the solution was simply overriding the behaviour to match my expectations:

(advice-add 'ghub--ident :override (lambda (username package) "credential"))

A curious reader might notice that this is not the end of the article. This is because a question as to why it didn't work in the first place was still open. After all, I can save the credential with the key of "username^forge", why wouldn't I be able to access it? SO I went for a little sidequest.

1password CLI

This is where I have decided to take a closer look at the library I was using for 1Password integration. What I have discovered that it's a very thin wrapper over op, the 1Password CLI. Specifically, it was leveraging the op read command.

Armed with this knowledge I tried to run op read and see what happens. Aaaand that failed spectacularly...

❯ op read "op://Personal/api.github.com/mishok13^forge"
[ERROR] 2025/11/16 20:42:04 could not read secret 'op://Personal/api.github.com/mishok13^forge': invalid secret reference 'op://Personal/api.github.com/mishok13^forge': invalid character in secret reference: '^'

This is the crux of the problem, as the apparently 1Password CLI has a very strict requirement for supported secret URIs. A correct reference according to 1Password is "op://Private/api.github.com/c3emmozqfyosisc6mwzwotz33a" and is obviously not something that auth-source can know ahead of time.

Seeing as op produces a clear error and has a fairly straightforward rule for allowed characters I decided to take another look at the auth-source-1password library.

Updating 1Password-auth-source

Initially I didn't pay too much attention to the library itself, but after toying with op and taking a closer look at the library I've encountered several things that didn't sit right with me:

Error handling: MIA

This one is perhaps the most striking -- the errors are gobbled up by the library and the result is then simply nil identifying a secret not found. I think it can be argued that a library should not simply silence the errors, however my experience with Emacs Lisp libraries is limited so maybe I'm missing something crucial. It is definitely supported by Emacs Lisp so it seems like an oversight on the author's part.

Sanitizing inputs

Another part is lack of sanitizing input, specifically in URIs passed to op read. That's something that could be addressed somewhat superficially by overriding auth-source-1password-construct-secret-reference but it would still leave the issue of having to adjust the names of secrets and fields to match the aforementioned limitations of the op:// URIs.

Using the wrong interface

This one is trickier and I can totally understand why one would not want to implement it. In essence, accessing vault items through op is... tedious. It involves manipulating op item API and it's clearly not as straightforward as firing off op read. But to be able to handle conflicts, correct search functionality and writing to the vaults it's the only way.

It's more steps though! Instead of a neat op read op://... you have to introduce some JSON parsing and logic in your code. For example, if I were to implement auth-source writing backend I would need to:

Note that this would require at least 2 op operations for every write request. Read would not fair any better as we would have to do a search and get and likely some validation.

In closing

After hopping through the hoops I've gotten to the point where forge works and yet I find myself almost never using it. I did manage to learn a thing or two about the inner workings of magit as well as discovering that defadvice is my friend.

And as for the auth-source-1password -- I've been chipping away at it at a glacial pace. Maybe one day it'll be good enough to be used.