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:
- Get all the items with
op item get --format jsonand parse JSON for the specific "host"- Item not found? Create new one
op item create --format json - Item is found? Make sure it has correct fields and if it doesn't, create a new one (same name but different template!)
- Multiple matching items found? Well, uhhhh, I guess flail and run?
- Item not found? Create new one
- Write the data using
op item edit
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.