Using a Private Plugin in a Claude Code Routine: Passing a PAT via Setup Script and Two Pitfalls

Introduction

I use Claude Code routines to collect information from external sites and generate a daily report. A routine is an agent that runs on a cron schedule in a cloud environment. The skills and subagents that create the report live as a plugin in a private GitHub repository.

I knew that I could copy the skills into the repository's .claude/ directory and it would work. But I manage skills and subagents centrally in the plugin repository, and keeping a copy would mean maintaining two sources. So the question I wanted to answer was whether a cloud routine could load a plugin from a private GitHub repository.

To cut to the chase, it works. However, it requires placing a GitHub token on the VM, and that led to two pitfalls.

This article covers three things:

  • How to load a private plugin in a cloud environment, and the steps involved
  • The approaches I considered but did not adopt, and why
  • Two pitfalls I ran into in operation: a token leak, and network blocking

It is aimed at anyone interested in running agents on a schedule in the cloud, not only Claude Code users.

Note that my plan is Max, and the details reflect the official documentation and behavior as of September 2026.

1. A cloud session does not inherit your local setup

When I first ran the routine, it reported that it could not find the skill. The plugin worked in my local Claude Code, so I had assumed my settings would carry over.

According to the official documentation (the "What carries over from your setup" section of cloud-environments), a cloud session starts from a fresh clone of the repository. The following do not carry over:

  • Plugins enabled in your local ~/.claude/settings.json (enabledPlugins at user scope)
  • enabledPlugins / extraKnownMarketplaces written in the repository's .claude/settings.json (these are not installed in the cloud)

On the other hand, the following directories committed to the repository are included in the clone and can be used:

  • .claude/skills/
  • .claude/agents/
  • .claude/commands/

This is why I had known that bundling skills was an option.

2. Passing a PAT to git through the setup script

A cloud environment has a "setup script." It is a Bash script that runs at the start of a session, before Claude Code starts. In mine, I set up git authentication and registered and updated the marketplace.

GH_PAT="<redacted>"
git config --global url."https://x-access-token:${GH_PAT}@github.com/".insteadOf "https://github.com/"

{
  echo "=== marketplace add (first time only; no-op if it already exists) ==="
  claude plugin marketplace add "https://x-access-token:${GH_PAT}@github.com/<org>/<repo>" 2>&1
  echo "=== marketplace update (refresh the cache to the latest main) ==="
  claude plugin marketplace update <marketplace> 2>&1
  echo "=== plugin install (first time only; no-op if it already exists) ==="
  claude plugin install <plugin>@<marketplace> --scope user 2>&1
  echo "=== plugin update (bring the installed plugin to the latest version) ==="
  claude plugin update <plugin>@<marketplace> 2>&1
} > /tmp/setup-diag.log 2>&1
cat /tmp/setup-diag.log
exit 0

add and install mean "install if missing," and update means "bring it up to date if present." The script does not run every time. According to the official documentation (the "Environment caching" section of cloud-environments), the result of the setup script is saved as a snapshot, and later sessions start from that snapshot. It runs again only when the script or the allowed domains change, or when the snapshot expires after about seven days. The trailing exit 0 ensures that a failure somewhere does not stop the session from starting.

I configured git authentication because of the following statement in the official documentation (the "Grant access to a private marketplace" section of plugins/host-marketplace). Claude Code clones a private marketplace using whatever git credentials the machine already has.

Claude Code runs git on their machine with interactive prompts turned off and relies on whatever credentials that machine already holds. Claude Code has no git token of its own, and marketplace.json has no field for one.

Unless the git on the VM somehow holds a token, a private marketplace cannot be fetched. Rewriting github.com URLs with insteadOf was the simplest way to meet that condition. This rewrite applies to all git operations against github.com. Even so, I have not seen it affect the routine's writes to the session's repository.

Looking at the execution logs, early runs loaded plugin version 0.7.0, and recent runs loaded 0.25.0. However, updates are not immediate. Version 0.22.0, which I merged on September 19, 2026, was still 0.21.3 in the run the next morning. The environment manager log shows that the setup script did not run.

{ "data": { "session_mode": "resume-cached" }, "event": "init_script_skipped" }

When I need an update right away, I add a comment line to the environment's setup script and save it. Because a change to the script triggers a rebuild, the next run picks up the latest version. I still run it this way.

Also, since insteadOf already handles authentication, the URL passed to marketplace add may not need the token. I have not verified this. Passing a URL with a token will leave it in settings.json (more on this in section 5).

Keep the token least-privileged

Since a token on the VM cannot be avoided, I want the damage from a leak to be small. The token I use is configured as follows:

  • Fine-grained PAT
  • Repository access: Only select repositories (just the one plugin repository)
  • Permissions: Contents set to Read-only, and Metadata set to Read-only (required)
  • An expiration date (one year)

With this setup, even if the token leaks, the damage is limited to someone reading the plugin repository. As described later, a leak did happen.

3. Approaches I considered and did not adopt

The environment's "API credentials" field does not work for GitHub

The environment settings have a field called "API credentials." The value is not placed on the VM. Instead, the agent's proxy attaches it when communicating. It looked like a way to avoid putting a token on the VM.

However, the official documentation (the "Requests that never get the credential" section of cloud-environments) says GitHub is excluded.

GitHub: the GitHub proxy authenticates requests to GitHub instead, so you don't need an API credential for it

Even if you register github.com, the credential is not attached to GitHub traffic. This field is meant for passing keys to APIs other than GitHub.

Letting the GitHub proxy handle it: not verified, so on hold

Access to GitHub in cloud environments goes through a dedicated GitHub proxy. The idea is that the VM's git uses limited credentials, and the proxy swaps in the real token when traffic leaves. If that works, a PAT would not be needed.

However, the documentation has the following restriction:

Repository scope: GitHub API and release-asset requests reach only repositories attached to the session

The proxy only reaches repositories attached to the session. It may work if I attach the plugin repository to the routine. But I could not confirm in the documentation whether git clone is subject to this restriction, or whether fetching the marketplace goes through the proxy. So I did not adopt this approach.

4. Set network access to Custom

Even if the plugin loads, whether a skill can read external sites is decided by a separate setting. I kept the default setting, and during that time, the body of no external site was ever retrieved (more on this in section 6).

The environment's network access has three options: Trusted (the default), Custom, and Full.

Trusted is not a "pass through official sites you trust" setting. It is a fixed allowlist that passes only the infrastructure needed for development work:

  • Package registries
  • Version control such as GitHub
  • Container registries
  • Cloud provider APIs
  • Anthropic's own domains

As a result, even an official corporate website is blocked unless it is on the list. For example, Google's list includes cloud.google.com and *.googleapis.com, but not blog.google or ai.google.dev.

I switched to "Custom" and added the domains I needed to "Allowed domains," one per line. I left "Also include default list of common package managers" checked. If you uncheck it, the default Trusted list is removed, and only the domains you wrote are allowed.

The following items are what I confirmed in the official documentation while setting this up:

Item Details
Where to configure On the routine's detail page, choose Edit from the menu, then select the cloud icon (environment name) below the Instructions field. Hover over the environment and open the settings icon to open the Update cloud environment dialog
Wildcards A leading *. matches all subdomains. The documentation does not say whether the parent domain itself (example.com) is included, so I wrote both example.com and *.example.com. www. is not included automatically
Traffic not subject to the allowlist GitHub traffic (via the GitHub proxy) and MCP connector traffic (via Anthropic's servers) pass regardless of the allowlist
When changes apply Changes apply from the next run. A session already in progress continues with the previous settings

I did not choose Full (which allows any domain). In an environment where a token sits on the VM, Full opens more routes through which a leaked token could be sent out.

5. Pitfall 1: one sentence in the instructions leaked the PAT into a log

The routine's execution log contained output in which the agent displayed ~/.claude/settings.json. The marketplace URL in it had the PAT embedded in it, in the form https://x-access-token:github_pat_…@github.com/….

There were two causes that combined:

  1. The extraKnownMarketplaces in the cloud environment's ~/.claude/settings.json contained a URL with the token in it. I believe the URL I had passed to claude plugin marketplace add in the setup script was recorded there.
  2. The skill's instructions said, "The output directory's environment variable is set in the env section of ~/.claude/settings.json." To verify the value, the agent displayed the whole file.
cat ~/.claude/settings.json 2>/dev/null | head -20

On the previous day's run, the agent used grep -s <variable name> ~/.claude/settings.json and displayed only the relevant line, so nothing leaked. Whether the whole file was displayed depended on the agent's behavior in each run.

I took two measures:

  • I reissued the token and replaced the value in the setup script
  • I rewrote the skill's instructions so the value is checked with echo "$VAR", and configuration files are never opened

The warning I added to the instructions reads:

Check values with echo "<variable name>". Do not open or cat configuration files such as ~/.claude/settings.json or ~/.gitconfig. In scheduled runs, marketplace URLs and similar values can contain credentials, and anything displayed is recorded in plaintext in the session log.

As long as a configuration file with the token embedded in it sits on the VM, one read is enough to leak it. This time, what triggered it was that my instructions pointed to the configuration file as the place where the value lives.

6. Pitfall 2: the report was written even though WebFetch was failing entirely

The second problem relates to the network setting I described in section 4. For about two and a half months after launch, the environment stayed on the default Trusted setting.

Part of the report (the section built from external sites) appeared and disappeared. When I investigated, I found that fetching the external APIs had failed every time. Whether the related section appeared depended on how hard the agent tried that day.

{"error_type":"EGRESS_BLOCKED","domain":"example.com","message":"Access to example.com is blocked by the network egress proxy."}

Every other external site I tried to read returned the same error. In the runs right after launch, the same kind of failure came back as HTTP 403 Forbidden.

The report was still being written because WebSearch was not blocked and kept working. The agent wrote the report using only the search result titles, excerpts, and URLs, and cited the URLs of the original pages as sources. Looking only at the sources, it appears that the original pages were read, but in fact the body of none of them was ever retrieved. I had believed "it used to work," but across the eight runs I examined, including the first day of operation, not once was a page body retrieved.

After this investigation, I changed the network access to Custom, as described in section 4.

7. If you want to build this in your own environment

Reproducing my setup requires the following four things:

  1. Issue a fine-grained PAT that covers only the plugin repository, with Contents and Metadata set to Read-only and an expiration date.
  2. In the environment's setup script, write the insteadOf git configuration and claude plugin marketplace add / plugin install (each with update as well). The results are cached for about seven days, so when you need a plugin update right away, add a comment line to the script to trigger a rebuild. You can check whether the script ran by looking for init_script_completed / init_script_skipped in /tmp/environment-manager-*.diag.log.
  3. Set network access to Custom, keep the default list included, and add the domains of the external sites your skill reads.
  4. Write in the skill's instructions "do not open configuration files," and have the agent check values with echo.

Step 4 is the countermeasure for the problem in section 5, where the token-containing marketplace URL I passed in step 2 remained in settings.json.

Closing

Private plugin repositories can be called from cloud routines. The condition is that you give the VM's git credentials through the setup script.

I will continue with this setup for now. I judged that a least-privileged PAT is sufficient. When the token did leak, the damage was limited to the scope of reading the plugin repository, and reissuing the token was enough to resolve it.

Open items remain:

  • Whether external site bodies can be retrieved on the first run after changing the network settings
  • Only 8 of about 80 runs were examined, so other runs may also have displayed the configuration file
  • The sites the routine reads change from day to day, so a Custom allowlist will always miss some. Whether to keep widening the allowlist or narrow the sources is undecided

There are two candidates for fundamentally removing the token from the VM, and neither has been started yet:

  • Bundle the skills in the repository's .claude/ directory. Then fetching the plugin is unnecessary, and neither the token nor the setup script is needed. The trade-off is maintaining two copies alongside the plugin.
  • On a Team or Enterprise plan, you can distribute a marketplace from Organization settings (Organization settings > Plugins & skills). It is read through the organization's GitHub integration, so git credentials are not involved. This is not expected to work on Pro or Max plans, and I have not checked whether plugins distributed by an organization are loaded by routines.

If you are building this from scratch, start with steps 1 and 2 in section 7: issuing the PAT and writing the setup script. If you already have this running, check whether your environment's network settings are still at the default (section 4), and whether any credentials appear in your execution logs (section 5).

All posts

No engineers on your team? Let’s start with what to build first.

Email me