Add support for reading the forgejo access token from a file #135

Closed
opened 2026-05-15 06:46:15 +00:00 by zzantares · 15 comments
zzantares commented 2026-05-15 06:46:15 +00:00 (Migrated from codeberg.org)

It would be nice to be able to provide the token from a file, this would avoid polluting the environment with credentials and be able to use sops/age decrypted secrets at run time instead.

Maybe it's already possible? though I wasn't able to figure out how to pass the token from a file, the {file:/path/to/token} syntax that works for other MCPs seemed not to work. Did I missed something?

Thanks!

It would be nice to be able to provide the token from a file, this would avoid polluting the environment with credentials and be able to use sops/age decrypted secrets at run time instead. Maybe it's already possible? though I wasn't able to figure out how to pass the token from a file, the `{file:/path/to/token}` syntax that works for other MCPs seemed not to work. Did I missed something? Thanks!
byteflavour commented 2026-05-15 11:20:41 +00:00 (Migrated from codeberg.org)

The MCP config for your agent is a file. That's one possible way to inject your credentials.

Setting your token as args[] should work.

Exclude the env block. (Only to show the complete example in the README.md)

{
  "mcpServers": {
    "forgejo": {
      "command": "forgejo-mcp",
      "args": [
        "--transport", "stdio",
        "--url", "https://your-forgejo-instance.org"
        "--token <your-token>"
      ],
      "env": {
        "FORGEJO_ACCESS_TOKEN": "<your personal access token>",
        "FORGEJO_USER_AGENT": "forgejo-mcp/1.0.0"
      }
    }
  }
}

So you should go with:

{
  "mcpServers": {
    "forgejo": {
      "command": "forgejo-mcp",
      "args": [
        "--transport", "stdio",
        "--url", "https://your-forgejo-instance.org"
        "--token <your-token>"
      ]
    }
  }
}

But this would maybe still populate your env with the credentials. A ps on this process may show something like:

forgejo-mcp --transport stdio --url https://your-forgejo-instance.org --token <your-token>

Therefore for me using sops to decrypt the key and inject it into the env was the more secure way. NixOS has wrapper build in for this but i am sure you can build this also in your setup.

A simple variant would be:

alias forgejo-mcp-alias = 'FORGEJO_ACCESS_TOKEN=$(cat path/to/your/token) forgejo-mcp'

The alias may not resolve the var at all. You can also try something like:

my-forgejo-mcp-call () { FORGEJO_ACCESS_TOKEN=$(cat path/to/your/token) forgejo-mcp; }

... this may work, haven't tested it. The call per se should work but setting this as alias you may need to tweak the var resolution a bit, the quotation.

This should start your process wrapped in it's own env including the token as env var.

Giving forgejo-mcp something like o local config file for one user is a bit against the nature of mcp, or? i mean it is thought as a remote service not as an app. The stdin is the way to go and addresses this kind of usage of "local setups" but mcp isn't meant that way. It's an api service paradigm.

The MCP config for your agent is a file. That's one possible way to inject your credentials. Setting your token as args[] should work. Exclude the env block. (Only to show the complete [example in the README.md](https://codeberg.org/goern/forgejo-mcp#3-configure-your-ai-assistant)) ```json { "mcpServers": { "forgejo": { "command": "forgejo-mcp", "args": [ "--transport", "stdio", "--url", "https://your-forgejo-instance.org" "--token <your-token>" ], "env": { "FORGEJO_ACCESS_TOKEN": "<your personal access token>", "FORGEJO_USER_AGENT": "forgejo-mcp/1.0.0" } } } } ``` So you should go with: ```json { "mcpServers": { "forgejo": { "command": "forgejo-mcp", "args": [ "--transport", "stdio", "--url", "https://your-forgejo-instance.org" "--token <your-token>" ] } } } ``` But this would **maybe** still populate your env with the credentials. A **ps** on this process may show something like: ```bash forgejo-mcp --transport stdio --url https://your-forgejo-instance.org --token <your-token> ``` Therefore for me using sops to decrypt the key and inject it into the env was the more secure way. NixOS has wrapper build in for this but i am sure you can build this also in your setup. A simple variant would be: ```bash alias forgejo-mcp-alias = 'FORGEJO_ACCESS_TOKEN=$(cat path/to/your/token) forgejo-mcp' ``` The alias may not resolve the var at all. You can also try something like: ```bash my-forgejo-mcp-call () { FORGEJO_ACCESS_TOKEN=$(cat path/to/your/token) forgejo-mcp; } ``` ... this may work, haven't tested it. The call per se should work but setting this as alias you may need to tweak the var resolution a bit, the quotation. This should start your process wrapped in it's own env including the token as env var. Giving forgejo-mcp something like o local config file for one user is a bit against the nature of mcp, or? i mean it is thought as a remote service not as an app. The stdin is the way to go and addresses this kind of usage of "local setups" but mcp isn't meant that way. It's an api service paradigm.
goern commented 2026-05-16 08:44:58 +00:00 (Migrated from codeberg.org)

Thanks @byteflavour for the thorough writeup.

We should not add a sops/age (or any secret-store) dependency to forgejo-mcp itself — secret decryption belongs in the wrapper layer around the MCP server (your shell function, NixOS wrapper, systemd unit, container entrypoint, etc.), not inside the binary.

So the path forward here is documentation, not implementation: extend the README with the wrapper patterns @byteflavour outlined (sops-wrapped env, shell function, etc.) and clarify that forgejo-mcp reads the token from FORGEJO_ACCESS_TOKEN or --token, full stop. Happy to take a docs PR.

Thanks @byteflavour for the thorough writeup. We should not add a sops/age (or any secret-store) dependency to `forgejo-mcp` itself — secret decryption belongs in the wrapper layer around the MCP server (your shell function, NixOS wrapper, systemd unit, container entrypoint, etc.), not inside the binary. So the path forward here is **documentation, not implementation**: extend the README with the wrapper patterns @byteflavour outlined (sops-wrapped env, shell function, etc.) and clarify that `forgejo-mcp` reads the token from `FORGEJO_ACCESS_TOKEN` or `--token`, full stop. Happy to take a docs PR.
byteflavour commented 2026-05-16 10:14:29 +00:00 (Migrated from codeberg.org)

I can only guess that @zzantares see's an security issue where non exists. In a proper setup one user on the same machine can't see processes or env vars of user. But yeah u populate your env with this credential and at least if u let act a agent with your access rights on your machine this entity sees your credentials. but putting it into a file don't makes it more secure ... letting any entity have access rights like the actual user then nothing safes against secrets leakage ... any way ...

I think this is out of scope also for documentation.

In a real world scenario the MCP is a service behind an api and every request contains the api key as payload. Therefore it's is out of scope.

Neither is it an edge case. Having the MCP configured like an app is an anti pattern IMHO.

But it is a interesting question!

The root cause here is that API keys are the worst access feature one can think of; especially long living ones but this is common practise ;)

So I see neither a case for documentation nor changes. It's out of scope. Maybe @zzantares can clearify the root cause.

I can only guess that @zzantares see's an security issue where non exists. In a proper setup one user on the same machine can't see processes or env vars of user. But yeah u populate your env with this credential and at least if u let act a agent with your access rights on your machine this entity sees your credentials. but putting it into a file don't makes it more secure ... letting any entity have access rights like the actual user then nothing safes against secrets leakage ... any way ... I think this is out of scope also for documentation. In a real world scenario the MCP is a service behind an api and every request contains the api key as payload. Therefore it's is out of scope. Neither is it an edge case. Having the MCP configured like an app is an anti pattern IMHO. But it is a interesting question! The root cause here is that API keys are the worst access feature one can think of; especially long living ones but this is common practise ;) So I see neither a case for documentation nor changes. It's out of scope. Maybe @zzantares can clearify the root cause.
zzantares commented 2026-05-23 07:11:20 +00:00 (Migrated from codeberg.org)

hey sorry for the late reply, my motivation wasn't really focused on security concerns but just for dx/convenience, I use HomeManager's programs.claude-code.mcpServers option to manage my settings, and I have a setup where nix-sops provides secrets in files, for other MCP servers I'm able to set the token like:

Authorization = "Bearer {file:${config.sops.secrets.token.path}}";

but that doesn't work with the forgejo-mcp, however, now that I think about it it might be because that's only possible when setting the token on headers not on envars/CLI options 🤔

In any case not a big deal, was just asking mostly out of convenience, and tbh I really don't like to populate secrets in the environment, one can set granular permissions on files, not so on envars.

Feel free to close 👍

hey sorry for the late reply, my motivation wasn't really focused on security concerns but just for dx/convenience, I use HomeManager's [`programs.claude-code.mcpServers`](https://home-manager-options.extranix.com/?query=programs.claude-code.mcpServers) option to manage my settings, and I have a setup where nix-sops provides secrets in files, for other MCP servers I'm able to set the token like: ``` Authorization = "Bearer {file:${config.sops.secrets.token.path}}"; ``` but that doesn't work with the forgejo-mcp, however, now that I think about it it might be because that's only possible when setting the token on headers not on envars/CLI options 🤔 In any case not a big deal, was just asking mostly out of convenience, and tbh I really don't like to populate secrets in the environment, one can set granular permissions on files, not so on envars. Feel free to close 👍
byteflavour commented 2026-05-24 00:35:59 +00:00 (Migrated from codeberg.org)

mhm ... interesting why u need to inject credentials into headers?!

u configure the mcp's by themself?

In a perfect world u would throw the mcp configs on your a.i. tools and these would initialize the handle. forgejo-mcp gives u the default way for this ... it's common style ... also nix handles this good ...

u could do something like:

mcp1 = { token = config.sops.placeholder.forgejo-token; }
programs.claude.mcp = mcp1; 

IDK ... it seems u have special setup.

Or is your problem aligned to #137?

mhm ... interesting why u need to inject credentials into headers?! u configure the mcp's by themself? In a perfect world u would throw the mcp configs on your a.i. tools and these would initialize the handle. forgejo-mcp gives u the default way for this ... it's common style ... also nix handles this good ... u could do something like: ```nix mcp1 = { token = config.sops.placeholder.forgejo-token; } programs.claude.mcp = mcp1; ``` IDK ... it seems u have special setup. Or is your problem aligned to #137?
zzantares commented 2026-05-26 18:08:43 +00:00 (Migrated from codeberg.org)

mhm ... interesting why u need to inject credentials into headers?!

what I need is to provide credentials from a file, I was saying that for other MCP servers I'm able to provide a file just fine but only seems to work when that's for an auth header, not cli options, not envars. Neither did work with forgejo-mcp.

u configure the mcp's by themself?

In a perfect world u would throw the mcp configs on your a.i. tools and these would initialize the handle. forgejo-mcp gives u the default way for this ... it's common style ... also nix handles this good ...

I'm not sure I understand what you mean by this, I have asked the agent to do this configuration, it can't because according to this the token can't be provided from a file which is what nix-sops produces for secrets. I opened the ticket after finding no way to make it work unless ofc polluting my environment with the token envar.

u could do something like:

mcp1 = { token = config.sops.placeholder.forgejo-token; }
programs.claude.mcp = mcp1; 

config.sops.placeholder.forgejo-token evaluates to a file path, whose contents are the unencrypted token, it's not the token itself, but if the token option can indeed accept either the token or a filepath to a file containing the token then that would solve my issue, I'm going to try this later when I get back home, though I'm pretty sure already tried tha before without luck, I'll double check.

Or is your problem aligned to #137?

Yeah probably, while I don't have that problem I think the proposed solution would allow me use it, provided provided that I can configure the auth headers like other mcps where I'm able to pass filepaths to the token instead of the token itself.

> mhm ... interesting why u need to inject credentials into headers?! what I need is to provide credentials from a file, I was saying that for other MCP servers I'm able to provide a file just fine but only seems to work when that's for an auth header, not cli options, not envars. Neither did work with forgejo-mcp. > u configure the mcp's by themself? > > In a perfect world u would throw the mcp configs on your a.i. tools and these would initialize the handle. forgejo-mcp gives u the default way for this ... it's common style ... also nix handles this good ... I'm not sure I understand what you mean by this, I have asked the agent to do this configuration, it can't because according to this the token can't be provided from a file which is what nix-sops produces for secrets. I opened the ticket after finding no way to make it work unless ofc polluting my environment with the token envar. > u could do something like: > > ```nix > mcp1 = { token = config.sops.placeholder.forgejo-token; } > programs.claude.mcp = mcp1; > ``` `config.sops.placeholder.forgejo-token` evaluates to a file path, whose contents are the unencrypted token, it's not the token itself, but if the token option can indeed accept either the token or a filepath to a file containing the token then that would solve my issue, I'm going to try this later when I get back home, though I'm pretty sure already tried tha before without luck, I'll double check. > Or is your problem aligned to #137? Yeah probably, while I don't have that problem I think the proposed solution would allow me use it, provided provided that I can configure the auth headers like other mcps where I'm able to pass filepaths to the token instead of the token itself.
byteflavour commented 2026-05-26 20:29:16 +00:00 (Migrated from codeberg.org)

Assuming you want to connect the forgejo-mcp server to your local agent tools like claude-code while you have forgejo-mcp either locally or - since last weekend - remotely ;) installed, the path to configure forgejo-mcp is configuring the calling tool.

Let's asume one uses emacs, gemini-cli or crush, while this are the tools i use. i can tell u that each of this tools are configured with a "file".

I use three different ways to encrypt and populate my secrets ...

  1. "Crush" ... the whole config is crypted. https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/secrets/crush.json - sops-nix decrypts the whole file on login.
  2. "Emacs" ... (special) Config is public but for the credentials emacs has it's own api. (authinfo) https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/modules/home/secrets.nix - on login the authinfo is generated
  3. Injecting secrets via env var into a wrapper ... https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/modules/home/ai/default.nix (this references to the decrypted key in the default location for the sops hm module /run/user//secrets.d//path/to/token/file)

As u can see i have no generic config path but all are more or less dedicated to a file ... IDE/Agent-tool configs, special credentials management or the root path utilizing native sops secrets management.

With a wrapper u don't populate your global user env with vars ... that's a bonus.

U have more possible pathes if u want your agentic tooling just call forgejo-mcp in the fashion of:

your tools mcp config = { "command": "forgejo-mcp", "env": [ null ], "args" = [ null], "anything_else" = [ null ] } ...

if i get u right ... u want a config file for forgejo-mcp.

E.g. forgejo-mcp-config.json

{
  "mcpServers": {
    "forgejo": {
      "command": "forgejo-mcp",
      "args": [
        "--transport", "stdio",
        "--url", "https://your-forgejo-instance.org"
      ],
      "env": {
        "FORGEJO_ACCESS_TOKEN": "<your personal access token>",
        "FORGEJO_USER_AGENT": "forgejo-mcp/1.0.0"
      }
    }
  }
}

I can still only guess why neither of this very common layers don't work for you ...

U said your agent wasn't able to provide a solution for you while u ask em to configure the STATELESS forgejo-mcp server for u.

Ask your agent to configure forgejo-mcp for the tool u use to chat with your agent! Allow me to roll my agent over this topic ... maybe gemini can better tell what i try to say ...

Assuming you want to connect the forgejo-mcp server to your local agent tools like claude-code while you have forgejo-mcp either locally or - since last weekend - remotely ;) installed, the path to configure forgejo-mcp is configuring the calling tool. Let's asume one uses emacs, gemini-cli or crush, while this are the tools i use. i can tell u that each of this tools are configured with a "file". I use three different ways to encrypt and populate my secrets ... 1. "Crush" ... the whole config is crypted. https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/secrets/crush.json - sops-nix decrypts the whole file on login. 2. "Emacs" ... (special) Config is public but for the credentials emacs has it's own api. (authinfo) https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/modules/home/secrets.nix - on login the authinfo is generated 3. Injecting secrets via env var into a wrapper ... https://codeberg.org/byteflavour/Derivation-Nation/src/branch/main/modules/home/ai/default.nix (this references to the decrypted key in the default location for the sops hm module /run/user/<uid>/secrets.d/<hm-derivation-id>/path/to/token/file) As u can see i have no generic config path but all are more or less dedicated to a file ... IDE/Agent-tool configs, special credentials management or the root path utilizing native sops secrets management. With a wrapper u don't populate your global user env with vars ... that's a bonus. U have more possible pathes if u want your agentic tooling just call forgejo-mcp in the fashion of: your tools mcp config = { "command": "forgejo-mcp", "env": [ null ], "args" = [ null], "anything_else" = [ null ] } ... if i get u right ... u want a config file for forgejo-mcp. E.g. forgejo-mcp-config.json ```json { "mcpServers": { "forgejo": { "command": "forgejo-mcp", "args": [ "--transport", "stdio", "--url", "https://your-forgejo-instance.org" ], "env": { "FORGEJO_ACCESS_TOKEN": "<your personal access token>", "FORGEJO_USER_AGENT": "forgejo-mcp/1.0.0" } } } } ``` I can still only guess why neither of this very common layers don't work for you ... U said your agent wasn't able to provide a solution for you while u ask em to configure the STATELESS forgejo-mcp server for u. Ask your agent to configure forgejo-mcp for the tool u use to chat with your agent! Allow me to roll my agent over this topic ... maybe gemini can better tell what i try to say ...
byteflavour commented 2026-05-26 20:34:32 +00:00 (Migrated from codeberg.org)

Hi @zzantares and @goern,

I am stepping in on behalf of @byteflavour to help clarify the situation. (I'm their friendly AI assistant, asked to review this thread and see if anything was missed!)

I've taken a look at byteflavour's local nixos-config and the discussion here, and I think there might be a slight misunderstanding about what is being requested and what it means for forgejo-mcp's codebase.

@byteflavour currently solves this exact problem by writing a custom Nix wrapper script that reads the sops-nix secret file and injects it as an environment variable before executing the server. For context, here is how it looks in their NixOS setup:

  forgejoMcpWrapper = pkgs.writeShellScriptBin "forgejo-mcp-wrapper" ''
    export FORGEJO_ACCESS_TOKEN=$(cat ${config.sops.secrets.codeberg_agent_api_key.path})
    exec ${pkgs.forgejo-mcp}/bin/forgejo-mcp --transport stdio --url https://codeberg.org "$@"
  '';

@zzantares is using Home Manager's programs.claude-code.mcpServers and wants to avoid writing and maintaining custom bash wrapper scripts for every single MCP server. Since sops-nix already decrypts the secret and places it as a plain-text file on the filesystem (e.g. /run/secrets/...), they just want to point the application to read that file directly instead of the raw string.

@goern, you rightly mentioned that forgejo-mcp should absolutely not include sops, age, or any secret-store dependencies. That is completely correct! However, supporting something like FORGEJO_ACCESS_TOKEN_FILE=/path/to/token.txt (or a --token-file CLI flag) does not require any crypto dependencies. It only requires a basic standard library file read.

This *_FILE environment variable pattern is a widely accepted industry standard for handling secrets gracefully (used extensively by Docker Secrets, systemd's LoadCredential, and many cloud-native tools like Postgres and MySQL). It allows the wrapper layer (like sops-nix) to do the heavy lifting of decrypting the secret to a filesystem path, while the application just seamlessly reads the plaintext directly from that path.

This small addition would completely eliminate the need for bash wrappers for NixOS/sops users, improving the DX significantly while keeping forgejo-mcp stateless and free of external secret-store dependencies.

Hopefully, this bridges the gap and explains why reading from a file is a great, standard compromise! Let us know what you think.

Cheers,
gemini-3.1-pro

Hi @zzantares and @goern, I am stepping in on behalf of @byteflavour to help clarify the situation. (I'm their friendly AI assistant, asked to review this thread and see if anything was missed!) I've taken a look at byteflavour's local `nixos-config` and the discussion here, and I think there might be a slight misunderstanding about what is being requested and what it means for `forgejo-mcp`'s codebase. @byteflavour currently solves this exact problem by writing a custom Nix wrapper script that reads the `sops-nix` secret file and injects it as an environment variable before executing the server. For context, here is how it looks in their NixOS setup: ```nix forgejoMcpWrapper = pkgs.writeShellScriptBin "forgejo-mcp-wrapper" '' export FORGEJO_ACCESS_TOKEN=$(cat ${config.sops.secrets.codeberg_agent_api_key.path}) exec ${pkgs.forgejo-mcp}/bin/forgejo-mcp --transport stdio --url https://codeberg.org "$@" ''; ``` @zzantares is using Home Manager's `programs.claude-code.mcpServers` and wants to avoid writing and maintaining custom bash wrapper scripts for every single MCP server. Since `sops-nix` already decrypts the secret and places it as a plain-text file on the filesystem (e.g. `/run/secrets/...`), they just want to point the application to read *that* file directly instead of the raw string. @goern, you rightly mentioned that `forgejo-mcp` should absolutely **not** include `sops`, `age`, or any secret-store dependencies. That is completely correct! However, supporting something like `FORGEJO_ACCESS_TOKEN_FILE=/path/to/token.txt` (or a `--token-file` CLI flag) does not require any crypto dependencies. It only requires a basic standard library file read. This `*_FILE` environment variable pattern is a widely accepted industry standard for handling secrets gracefully (used extensively by Docker Secrets, systemd's `LoadCredential`, and many cloud-native tools like Postgres and MySQL). It allows the wrapper layer (like `sops-nix`) to do the heavy lifting of decrypting the secret to a filesystem path, while the application just seamlessly reads the plaintext directly from that path. This small addition would completely eliminate the need for bash wrappers for NixOS/sops users, improving the DX significantly while keeping `forgejo-mcp` stateless and free of external secret-store dependencies. Hopefully, this bridges the gap and explains why reading from a file is a great, standard compromise! Let us know what you think. Cheers, gemini-3.1-pro
byteflavour commented 2026-05-26 20:46:57 +00:00 (Migrated from codeberg.org)

If this is near your solution ... @zzantares ... what gemini still not get ... sops-nix decrypts the secrets in dynamic location.

/run/user//secrets.d//path/to/token ...

so that's not the solution? but let's stick to the problem ... it may solve the problem for the 99% but not for the user, my-friendly-ai-assistent.

If this is near your solution ... @zzantares ... what gemini still not get ... sops-nix decrypts the secrets in dynamic location. /run/user/<user-id>/secrets.d/<DYNAMIC-DERIVATION-ID>/path/to/token ... so that's not the solution? but let's stick to the problem ... it may solve the problem for the 99% but not for the user, my-friendly-ai-assistent.
byteflavour commented 2026-05-26 20:47:45 +00:00 (Migrated from codeberg.org)

Ah, let me clarify the dynamic path part! (gemini-3.1-pro here again 🤖)

It's absolutely true that sops-nix uses dynamic paths (like /run/user/<id>/secrets.d/<hash>/...). However, in the context of Nix/Home Manager, this is exactly what config.sops.secrets.token.path solves!

When Home Manager evaluates your configuration, it knows exactly what that dynamic path will be for that specific generation. Because @zzantares is configuring the MCP server through Home Manager (programs.claude-code.mcpServers), Nix will template that exact dynamic path directly into Claude's JSON config file.

So, if forgejo-mcp supported --token-file, the Nix config would look like this:

programs.claude-code.mcpServers.forgejo = {
  command = "forgejo-mcp";
  args = [
    "--transport" "stdio"
    "--url" "https://codeberg.org"
    "--token-file" config.sops.secrets.token.path
  ];
};

And Home Manager would generate a claude.json that looks like this:

{
  "mcpServers": {
    "forgejo": {
      "command": "forgejo-mcp",
      "args": [
        "--transport", "stdio",
        "--url", "https://codeberg.org",
        "--token-file", "/run/user/1000/secrets.d/a1b2c3d4/token"
      ]
    }
  }
}

Whenever the sops-nix generation changes and the secret gets a new dynamic path, Home Manager simultaneously regenerates the claude.json config file with the new path. They always stay perfectly in sync!

So a --token-file argument (or FORGEJO_ACCESS_TOKEN_FILE env var) actually solves the problem elegantly for Nix users, without requiring forgejo-mcp to know anything about Sops or dynamic paths. The caller (Claude, configured by Home Manager) simply hands the correct, current file path to the server.

Would a PR adding --token-file and FORGEJO_ACCESS_TOKEN_FILE be welcome, @goern? It's a very small standard addition that makes the tool much more flexible for these declarative setups.

Ah, let me clarify the dynamic path part! (gemini-3.1-pro here again 🤖) It's absolutely true that `sops-nix` uses dynamic paths (like `/run/user/<id>/secrets.d/<hash>/...`). However, in the context of Nix/Home Manager, this is exactly what `config.sops.secrets.token.path` solves! When Home Manager evaluates your configuration, it knows exactly what that dynamic path will be for that specific generation. Because @zzantares is configuring the MCP server *through* Home Manager (`programs.claude-code.mcpServers`), Nix will template that exact dynamic path directly into Claude's JSON config file. So, if `forgejo-mcp` supported `--token-file`, the Nix config would look like this: ```nix programs.claude-code.mcpServers.forgejo = { command = "forgejo-mcp"; args = [ "--transport" "stdio" "--url" "https://codeberg.org" "--token-file" config.sops.secrets.token.path ]; }; ``` And Home Manager would generate a `claude.json` that looks like this: ```json { "mcpServers": { "forgejo": { "command": "forgejo-mcp", "args": [ "--transport", "stdio", "--url", "https://codeberg.org", "--token-file", "/run/user/1000/secrets.d/a1b2c3d4/token" ] } } } ``` Whenever the `sops-nix` generation changes and the secret gets a new dynamic path, Home Manager *simultaneously* regenerates the `claude.json` config file with the new path. They always stay perfectly in sync! So a `--token-file` argument (or `FORGEJO_ACCESS_TOKEN_FILE` env var) actually solves the problem elegantly for Nix users, without requiring `forgejo-mcp` to know anything about Sops or dynamic paths. The caller (Claude, configured by Home Manager) simply hands the correct, current file path to the server. Would a PR adding `--token-file` and `FORGEJO_ACCESS_TOKEN_FILE` be welcome, @goern? It's a very small standard addition that makes the tool much more flexible for these declarative setups.
byteflavour commented 2026-05-26 21:04:30 +00:00 (Migrated from codeberg.org)

ok u say it's industry standard ... name 3 relevant mcp's which introduce --token-file as arg.
what is the difference between setting the token-file in the mcp config and the token directly?

If it is a common UX pattern or industry standard owners and/or maintainers may put effort to introduce the feature but i still don't get how it changes the world to a better and i don't see this standard out there.

so pls name the 3 relevant mcp's introduce this flag against the nature of a mcp :-P

but anyway ... i am open to develop it, if this is the solution @goern and @zzantares.

ok u say it's industry standard ... name 3 relevant mcp's which introduce --token-file as arg. what is the difference between setting the token-file in the mcp config and the token directly? If it is a common UX pattern or industry standard owners and/or maintainers may put effort to introduce the feature but i still don't get how it changes the world to a better and i don't see this standard out there. so pls name the 3 relevant mcp's introduce this flag against the nature of a mcp :-P but anyway ... i am open to develop it, if this is the solution @goern and @zzantares.
byteflavour commented 2026-05-26 21:05:43 +00:00 (Migrated from codeberg.org)

Hello again! (gemini-3.1-pro here 🤖)

That is a completely fair challenge! To clarify, when I said "industry standard," I was referring to general Linux/container applications (like Docker Secrets using POSTGRES_PASSWORD_FILE, systemd's LoadCredential, etc.), rather than MCP servers specifically. The MCP ecosystem is still quite young, so we are all figuring out the best practices together!

To answer your crucial question: What is the difference between setting the token directly vs. setting the file path in the config?

For a Nix/Home Manager user, this is actually a major security distinction:

  1. Setting the token directly: If a Nix user puts the raw token into their Home Manager config so it evaluates into the claude.json file, that raw token gets written in plain text to the /nix/store (which is world-readable by default!).
  2. Setting the token-file path: If they put the file path (e.g., /run/user/1000/secrets.d/.../token) into the config, the generated claude.json in the /nix/store only contains the path string. The actual secret remains securely locked in the RAM-backed sops tmpfs, protected by strict Linux file permissions.

Your Bash wrapper approach solved exactly this problem by reading the file at runtime instead of build time. Adding a --token-file or FORGEJO_ACCESS_TOKEN_FILE argument just allows forgejo-mcp to natively do that runtime file-read, saving users from having to write wrappers.

I completely agree that it is up to @goern and @zzantares to decide if this is a pattern they want to adopt for forgejo-mcp. If they do, it sounds like you're ready to help build it, which is awesome! 🚀

Hello again! (gemini-3.1-pro here 🤖) That is a completely fair challenge! To clarify, when I said "industry standard," I was referring to general Linux/container applications (like Docker Secrets using `POSTGRES_PASSWORD_FILE`, systemd's `LoadCredential`, etc.), rather than MCP servers specifically. The MCP ecosystem is still quite young, so we are all figuring out the best practices together! To answer your crucial question: **What is the difference between setting the token directly vs. setting the file path in the config?** For a Nix/Home Manager user, this is actually a major security distinction: 1. **Setting the token directly:** If a Nix user puts the raw token into their Home Manager config so it evaluates into the `claude.json` file, that raw token gets written in plain text to the `/nix/store` (which is world-readable by default!). 2. **Setting the token-file path:** If they put the *file path* (e.g., `/run/user/1000/secrets.d/.../token`) into the config, the generated `claude.json` in the `/nix/store` only contains the path string. The actual secret remains securely locked in the RAM-backed `sops` tmpfs, protected by strict Linux file permissions. Your Bash wrapper approach solved exactly this problem by reading the file at *runtime* instead of build time. Adding a `--token-file` or `FORGEJO_ACCESS_TOKEN_FILE` argument just allows `forgejo-mcp` to natively do that runtime file-read, saving users from having to write wrappers. I completely agree that it is up to @goern and @zzantares to decide if this is a pattern they want to adopt for `forgejo-mcp`. If they do, it sounds like you're ready to help build it, which is awesome! 🚀
byteflavour commented 2026-05-26 21:14:30 +00:00 (Migrated from codeberg.org)

yeah ... it's not a pattern ... syntactic sugar it is maybe ... i am not sure here but as i said ... have no problem to implement that.

yeah ... it's not a pattern ... syntactic sugar it is maybe ... i am not sure here but as i said ... have no problem to implement that.
decarvalhoaa commented 2026-05-26 22:01:36 +00:00 (Migrated from codeberg.org)

If I may jump in...

I suspect that @zzantares is trying to avoid leaking secrets.

The token will be visible to any one that can run docker inspect when the token is passed via FORGEJO_ACCESS_TOKEN env var, or ps aux for the cli equivalent. For that reason many applications support a *_FILE env var or cli argument equivalent to pass a file path to the file containing the secret. Usually in docker, the *_FILE env var is used in combination with docker secrets. Then the env var can be defined as something like `FORGEJO_ACCESS_TOKEN_FILE=/run/secrets/forgejo_mcp_token.

I have build my own image for this application and added a entrypoint.sh wrapper to read from the file path if the official FORGEJO_ACCESS_TOKEN is not set or empty.

#!/bin/sh
set -eu

# Initialize the command with the base executable
set -- /app/forgejo-mcp

# Append arguments based on custom environment variables
# The app does not currently document env vars for transport and ports,
# so the wrapper maps these env vars to CLI flags.
if [ -n "${FORGEJO_TRANSPORT:-}" ]; then
    set -- "$@" --transport "${FORGEJO_TRANSPORT}"
fi

if [ -n "${FORGEJO_HTTP_PORT:-}" ]; then
    set -- "$@" --http-port "${FORGEJO_HTTP_PORT}"
fi

if [ -n "${FORGEJO_SSE_PORT:-}" ]; then
    set -- "$@" --sse-port "${FORGEJO_SSE_PORT}"
fi

# Docker secret support for FORGEJO_ACCESS_TOKEN
# The application natively reads FORGEJO_ACCESS_TOKEN.
# The wrapper adds FORGEJO_ACCESS_TOKEN_FILE support.
if [ -z "${FORGEJO_ACCESS_TOKEN:-}" ] && [ -n "${FORGEJO_ACCESS_TOKEN_FILE:-}" ] && [ -f "${FORGEJO_ACCESS_TOKEN_FILE}" ]; then
    export FORGEJO_ACCESS_TOKEN="$(tr -d '\r\n' < "${FORGEJO_ACCESS_TOKEN_FILE}")"
fi

# Execute the final constructed command
# Use 'exec' so the application becomes PID 1 and receives signals correctly.
echo "[entrypoint] Launching Forgejo MCP"
echo "[entrypoint] transport=${FORGEJO_TRANSPORT:-stdio}"
echo "[entrypoint] http_port=${FORGEJO_HTTP_PORT:-8080}"
echo "[entrypoint] sse_port=${FORGEJO_SSE_PORT:-8080}"
echo "[entrypoint] url=${FORGEJO_URL:-unset}"
echo "[entrypoint] debug=${FORGEJO_DEBUG:-app-default}"
echo "[entrypoint] token_configured=$([ -n "${FORGEJO_ACCESS_TOKEN:-}" ] && echo true || echo false)"

exec "$@"
If I may jump in... I suspect that @zzantares is trying to avoid leaking secrets. The token will be visible to any one that can run `docker inspect` when the token is passed via `FORGEJO_ACCESS_TOKEN` env var, or `ps aux` for the cli equivalent. For that reason many applications support a `*_FILE` env var or cli argument equivalent to pass a file path to the file containing the secret. Usually in docker, the `*_FILE` env var is used in combination with docker secrets. Then the env var can be defined as something like `FORGEJO_ACCESS_TOKEN_FILE=/run/secrets/forgejo_mcp_token. I have build my own image for this application and added a `entrypoint.sh` wrapper to read from the file path if the official `FORGEJO_ACCESS_TOKEN` is not set or empty. ``` #!/bin/sh set -eu # Initialize the command with the base executable set -- /app/forgejo-mcp # Append arguments based on custom environment variables # The app does not currently document env vars for transport and ports, # so the wrapper maps these env vars to CLI flags. if [ -n "${FORGEJO_TRANSPORT:-}" ]; then set -- "$@" --transport "${FORGEJO_TRANSPORT}" fi if [ -n "${FORGEJO_HTTP_PORT:-}" ]; then set -- "$@" --http-port "${FORGEJO_HTTP_PORT}" fi if [ -n "${FORGEJO_SSE_PORT:-}" ]; then set -- "$@" --sse-port "${FORGEJO_SSE_PORT}" fi # Docker secret support for FORGEJO_ACCESS_TOKEN # The application natively reads FORGEJO_ACCESS_TOKEN. # The wrapper adds FORGEJO_ACCESS_TOKEN_FILE support. if [ -z "${FORGEJO_ACCESS_TOKEN:-}" ] && [ -n "${FORGEJO_ACCESS_TOKEN_FILE:-}" ] && [ -f "${FORGEJO_ACCESS_TOKEN_FILE}" ]; then export FORGEJO_ACCESS_TOKEN="$(tr -d '\r\n' < "${FORGEJO_ACCESS_TOKEN_FILE}")" fi # Execute the final constructed command # Use 'exec' so the application becomes PID 1 and receives signals correctly. echo "[entrypoint] Launching Forgejo MCP" echo "[entrypoint] transport=${FORGEJO_TRANSPORT:-stdio}" echo "[entrypoint] http_port=${FORGEJO_HTTP_PORT:-8080}" echo "[entrypoint] sse_port=${FORGEJO_SSE_PORT:-8080}" echo "[entrypoint] url=${FORGEJO_URL:-unset}" echo "[entrypoint] debug=${FORGEJO_DEBUG:-app-default}" echo "[entrypoint] token_configured=$([ -n "${FORGEJO_ACCESS_TOKEN:-}" ] && echo true || echo false)" exec "$@" ```
byteflavour commented 2026-05-30 12:04:00 +00:00 (Migrated from codeberg.org)

Assuming you want to deploy forgejo-mcp as 12-factor app, build for scaling ... why should this app get authenticated against one or multiple forgejo instances with exactly one PAT?

It's an interesting and important question, how mature forgejo-mcp is for hyper scaling but this is another topic. But i can't imagine the outcome will be: "Oh yeah we need our horizontal hyper scaling stateless hundreds of thousands requests processing infrastructure unlocked with one personal access token." IMHO: It's the mother of anti pattern to handle a PAT.

forgejo-mcp can now operate as distributed service. this leads finally to:

forgejo-mcp acts like a mcp server! the configuration wether it is a file or any other configuration path it is the client which is entirely responsible for the configuration.

Assuming you want to deploy forgejo-mcp as 12-factor app, build for scaling ... why should this app get authenticated against one or multiple forgejo instances with exactly one PAT? It's an interesting and important question, how mature forgejo-mcp is for hyper scaling but this is another topic. But i can't imagine the outcome will be: "Oh yeah we need our horizontal hyper scaling stateless hundreds of thousands requests processing infrastructure unlocked with one personal access token." IMHO: It's the mother of anti pattern to handle a PAT. forgejo-mcp can now operate as distributed service. this leads finally to: forgejo-mcp acts like a mcp server! the configuration wether it is a file or any other configuration path it is the client which is entirely responsible for the configuration.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
agentic-forges/forgejo-mcp#135
No description provided.