Skip to content

command to retreive token#124

Open
pigam wants to merge 7 commits into
git-pkgs:mainfrom
pigam:main
Open

command to retreive token#124
pigam wants to merge 7 commits into
git-pkgs:mainfrom
pigam:main

Conversation

@pigam

@pigam pigam commented Jun 16, 2026

Copy link
Copy Markdown

closes #67

Instead of storing a token in plain text, a config entry can now reference a shell command prefixed with !. The command is executed at runtime and its stdout is used as the token, enabling integration with password managers such as rbw or pass.

forge auth login gains a Ctrl+E shortcut at the token prompt to enter a command interactively.

forge auth status displays the command.

@hramrach

Copy link
Copy Markdown

It would be nice to have an option to specify hostname or URL in the key command, and use the same command for all forges.

Some password databases come organized like that.

@pigam

pigam commented Jun 16, 2026

Copy link
Copy Markdown
Author

It would be nice to have an option to specify hostname or URL in the key command, and use the same command for all forges.

Some password databases come organized like that.

Can you give an example of what you want ?

For the moment :
token = value or token = !command. You want to add interpolation of other variables in the command, or is it something else ?
An example of what you image would be great !

@hramrach

Copy link
Copy Markdown

eg. in here

https://github.com/openSUSE/kernel-source/blob/b572de5eb963a125f55515c346881fd1e05caaf6/scripts/python/obsapi/obsapi.py#L121

You can see subprocess.check_output(['secret-tool', 'lookup', 'service', host, 'username', self.user])

That is same command used for every forge, and looked up by the 'service' tag. The 'host' argument is the host name of the forge.

This is a database that is arbitrarily created like this by another tool, the secret-tool is generic tagged storage that does not itself interpret the tags in any way.

@hramrach

Copy link
Copy Markdown

Your solution still makes it possible (hopefully) to manually configure for each forge something like

token = !secret-tool lookup service github.com type token

or somesuch but does not provide the option to configure such command globally replacing the 'github.com' with the hostname of the forge.

@pigam

pigam commented Jun 17, 2026

Copy link
Copy Markdown
Author

the command has now the environment FORGE_DOMAIN set to the domain, thanks @hramrach for the idea !

so your example can be written as:

token = !secret-tool lookup service $FORGE_DOMAIN type token

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking up #67. The ! prefix matches what people know from git's credential.helper, and the allowTokens=false guard correctly stops a checked-in .forge from running arbitrary commands, which is the obvious trap here. Good that there's an explicit test for it.

The main thing blocking this is the execution model: token commands run eagerly at config parse time, for every domain, on every invocation. See inline comments. Making resolution lazy (store the raw value, exec only when that domain's token is actually needed) fixes most of the issues at once.

Small thing: "retreive" → "retrieve" in the PR title.

Comment thread internal/config/config.go
Comment on lines +198 to +207
if strings.HasPrefix(v, "!") {
resolved, err := execValue(v[1:], name)
if err != nil {
return fmt.Errorf("%s: [%s] token command: %w", path, name, err)
}
ds.Token = resolved
ds.TokenExec = v
} else {
ds.Token = v
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs at parse time, and config.Load() is called from rootCmd.PersistentPreRun (internal/cli/root.go:30) to read the default output format. So every forge invocation, including forge --help and forge auth login, executes the token command for every configured domain, not just the one being used.

With pass (GPG pinentry) or rbw (vault unlock) that means being challenged for unrelated domains on every command.

It also means a single failing command (vault locked, binary missing, typo) makes loadFile return an error, which propagates through resolve.go:196/276/290 and breaks every forge operation regardless of which host you're targeting.

I'd store the raw !cmd here and resolve lazily, e.g. a (*DomainSection).ResolveToken() called from resolve.TokenForDomain only when that domain is actually in use.

Comment thread internal/config/config.go Outdated
Comment on lines +107 to +113
c := exec.Command("sh", "-c", cmd)
c.Env = append(os.Environ(), "FORGE_DOMAIN="+domain)
out, err := c.Output()
if err != nil {
return "", fmt.Errorf("%q: %w", cmd, err)
}
return strings.TrimSpace(string(out)), nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here:

  1. sh -c assumes sh is in PATH. CI passes on windows-latest because GitHub runners ship Git Bash, but a stock Windows install won't have it. The codebase already branches on runtime.GOOS == "windows" elsewhere; either fall back to cmd /C or document this as Unix-only.

  2. c.Output() captures stderr into *exec.ExitError.Stderr but the %w wrapping below doesn't surface it, so the user just sees "rbw get foo": exit status 1 with no detail. Worth extracting and including in the error. Also consider wiring c.Stdin = os.Stdin / c.Stderr = os.Stderr so password managers that prompt on the terminal (pinentry-tty, rbw unlock) can actually do so.

Comment thread internal/cli/auth.go
Comment on lines +121 to +157
func readRawToken(fd int, oldState *term.State, r io.Reader) (string, error) {
const (
ctrlC = 0x03
ctrlD = 0x04
enter = 0x0D
newline = 0x0A
backspace = 0x7F
del = 0x08
printable = 0x20
)
defer func() {
_ = term.Restore(fd, oldState)
_, _ = fmt.Fprintln(os.Stderr)
}()

var buf []byte
b := make([]byte, 1)
for {
if _, err := r.Read(b); err != nil {
return "", err
}

switch b[0] {
case ctrlC, ctrlD:
return "", fmt.Errorf("interrupted")
case enter, newline:
return strings.TrimSpace(string(buf)), nil
case backspace, del:
if len(buf) > 0 {
buf = buf[:len(buf)-1]
}
default:
if b[0] >= printable {
buf = append(buf, b[0])
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reimplements password input byte-by-byte to support the Ctrl+E peek, but it's a regression from term.ReadPassword: arrow keys send \x1b[D etc., the 0x1b is filtered (< 0x20) but [ and D are appended to the token. term.ReadPassword also handled Ctrl+U / Ctrl+W.

A --token-cmd 'rbw get x' flag would be simpler, scriptable, and avoid the raw-mode code entirely. The Ctrl+E shortcut can stay as sugar if you like, but I'd lead with the flag in the README.

Comment thread internal/cli/auth.go Outdated
// Returns the command prefixed with "!" for storage in the config.
func readCommandInteractive(domain string) (string, error) {
_, _ = fmt.Fprintf(os.Stderr, "Command for token (e.g. rbw get %s): ", domain)
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: error from ReadString is discarded.

Comment thread internal/cli/auth.go Outdated
Comment on lines +204 to +205
if cfgSection.TokenExec != "" {
sources = append(sources, fmt.Sprintf("config (cmd: %s)", cfgSection.TokenExec))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenExec stores the raw value including the leading !, so this prints config (cmd: !echo secret). The ! is config syntax, not part of the command; either strip it for display or store v[1:] in TokenExec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: execute command to retrieve token

3 participants