·10 min read

Publish AI Skills from CI/CD with OIDC (No Secrets Required)

Use OpenID Connect to publish AI coding skills from GitHub Actions and GitLab CI without storing API tokens as secrets. No rotation, no leaks, no long-lived credentials.

The problem with API tokens in CI/CD

Every developer who has automated publishing knows the drill: generate a long-lived API token, paste it into your CI secret store, and cross your fingers that it never leaks. Long-lived tokens are a liability. They don't expire, they get copied between engineers, and when someone leaves the team, rotating them is a manual chore that often gets skipped.

For publishing AI coding skills and rules from CI/CD pipelines, this problem gets worse. You might have a dozen repositories, each needing to publish updated skills after merging to main. That's a dozen places to store a token, a dozen rotation events to track, and a dozen blast-radius scenarios if any of them leak.

OpenID Connect (OIDC) removes this problem. Instead of a static token, your CI provider issues a short-lived, cryptographically signed identity token for each workflow run. localskills.sh verifies that token against a trust policy you configure - no secrets to store, no rotation to manage.

This guide walks through setting up OIDC-based publishing for both GitHub Actions and GitLab CI.

What is OIDC in CI/CD?

OIDC is an identity layer built on OAuth 2.0. You've used it every time you clicked "Sign in with Google." In the CI/CD context, the same mechanism lets your pipeline prove its identity to external services.

Here's how the flow works:

  1. Your CI job runs - GitHub Actions or GitLab CI generates a signed JWT that encodes facts about the run: which repository, which branch, which environment, which actor triggered it.
  2. The job requests a token - the runner calls GitHub's or GitLab's OIDC token endpoint to get this signed JWT.
  3. The job presents the token - it calls the localskills.sh API with this JWT instead of an API key.
  4. localskills.sh verifies the token - it checks the signature against the provider's public keys, then evaluates your trust policy (repo, branch, environment conditions).
  5. If all checks pass, the publish proceeds - the token expires after minutes; there's nothing to rotate.

The key difference from long-lived tokens: there's nothing to steal from your secret store because there's nothing in your secret store.

Configuring a trust policy on localskills.sh

Before touching your pipeline, create a trust policy in the localskills.sh dashboard. Trust policies live on your team account and control which CI runs are allowed to publish.

Navigate to Settings > OIDC Trust Policies > Add policy and fill in:

FieldDescriptionExample
ProviderYour CI platformgithub or gitlab
RepositoryRepo allowed to publishmyorg/my-repo
Branch filterWhich refs can publishrefs/heads/main
EnvironmentOptional environment gateproduction
Skill scopeWhich skills this policy can publishmyorg/* or myorg/specific-skill

Branch filtering is important. Without it, any branch in your repo could publish. Locking to refs/heads/main ensures that only merged code reaches your published skills - not a PR branch someone pushed to test something.

Environment gating adds another layer. GitHub Actions environments have their own approval workflows and secret stores. Requiring an environment means the publish can only happen after any environment-level approvals are satisfied.

You can create multiple policies - one for each repository that should publish, each scoped to exactly what it needs.

GitHub Actions: step-by-step setup

Here's a complete workflow that publishes a skill after every push to main.

# .github/workflows/publish-skill.yml
name: Publish AI skill

on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required: allows the job to request an OIDC token
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: production   # Match this to your trust policy's environment field

    steps:
      - uses: actions/checkout@v4

      - name: Install localskills CLI
        run: npm install -g @localskills/cli

      - name: Publish skill via OIDC
        run: localskills publish --oidc
        env:
          LOCALSKILLS_TEAM: myorg   # Your localskills team slug

The critical piece is permissions: id-token: write. Without it, the runner cannot request an OIDC token at all - GitHub blocks the request by default. Adding this permission scoped to just the job that needs it (rather than the whole workflow) follows the principle of least privilege.

The --oidc flag tells the CLI to request a token from the GitHub OIDC endpoint (ACTIONS_ID_TOKEN_REQUEST_URL) instead of looking for a LOCALSKILLS_TOKEN environment variable. The CLI handles the token exchange automatically.

What happens if the trust policy doesn't match?

If the repository, branch, or environment in the JWT doesn't match any trust policy on your team, the publish fails with a clear error:

Error: OIDC trust policy verification failed.
  Repository: myorg/my-repo ✓
  Branch: refs/heads/feature-branch ✗ (policy requires refs/heads/main)

This makes debugging straightforward - the mismatch is explicit, not a vague authentication error.

GitLab CI: step-by-step setup

GitLab's OIDC support works through the id_tokens keyword introduced in GitLab 15.7. The setup is similarly clean.

# .gitlab-ci.yml
publish-skill:
  stage: deploy
  image: node:20-alpine
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

  id_tokens:
    LOCALSKILLS_OIDC_TOKEN:
      aud: "https://localskills.sh"   # Must match the expected audience

  script:
    - npm install -g @localskills/cli
    - localskills publish --oidc --oidc-token $LOCALSKILLS_OIDC_TOKEN
  variables:
    LOCALSKILLS_TEAM: myorg

GitLab's id_tokens block generates the JWT and injects it as an environment variable. The aud (audience) field must exactly match what localskills.sh expects - https://localskills.sh. This prevents tokens issued for one service from being replayed against another.

GitLab also has an environments feature (using the environment: keyword in .gitlab-ci.yml), and the OIDC JWT includes an environment claim when the job specifies one. Configure your trust policy to match on the environment claim if you use GitLab Environments, or scope purely by project and branch.

GitLab trust policy fields

When creating a GitLab trust policy in the localskills.sh dashboard, the relevant claims are:

ClaimMaps toExample
namespace_pathGitLab group or usermygroup
project_pathFull project pathmygroup/my-project
refBranch or tag refmain
ref_typebranch or tagbranch
environmentGitLab environment nameproduction

Combining project_path and ref on main with ref_type of branch gives you the same protection as the GitHub setup - only merged code publishes.

Advanced trust policy patterns

Publishing only from tags

If you version your skills and want to publish only on release tags, update the trust policy and workflow accordingly:

# GitHub Actions -- publish on version tags
on:
  push:
    tags: ["v*"]

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @localskills/cli
      - run: localskills publish --oidc --version ${{ github.ref_name }}
        env:
          LOCALSKILLS_TEAM: myorg

Set the trust policy branch filter to refs/tags/v* (glob patterns are supported). Now only version tags trigger a publish, and the version number flows from the tag name automatically.

This integrates naturally with semantic versioning for skills - tag v1.2.0, get version 1.2.0 published without any manual work.

Monorepo: publish multiple skills from one pipeline

Many teams store their AI skills alongside the code they govern - a monorepo pattern where each package has its own skill definition. OIDC works just as well here:

# GitHub Actions -- monorepo, publish changed skills only
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    environment: production
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2   # Need parent commit to detect changes

      - name: Install localskills CLI
        run: npm install -g @localskills/cli

      - name: Publish changed skills
        run: |
          # Find skill.json files that changed in this push
          git diff --name-only HEAD~1 HEAD | grep 'skill\.json$' | while read skillfile; do
            skilldir=$(dirname "$skillfile")
            echo "Publishing skill in $skilldir"
            localskills publish --oidc --dir "$skilldir"
          done
        env:
          LOCALSKILLS_TEAM: myorg

One trust policy covers all skills from the monorepo - you scope the policy to myorg/* rather than a specific skill name.

Security best practices

OIDC is more secure than long-lived tokens by default, but there are still ways to configure it poorly. A few rules to follow:

Scope trust policies as narrowly as possible. A policy that allows any branch from any repo to publish any skill is barely better than a shared token. Pin to refs/heads/main, specific project paths, and skill name globs that match only what that repo owns.

Use environments for sensitive publishes. If your published skill is installed by many teams, require a GitHub environment with manual approval before publish. This adds a human gate between a merge and a publish.

Audit publish events. localskills.sh logs every publish with the full OIDC claims that authorized it - which repo, which branch, which run ID. If something unexpected is published, you can trace it exactly. Check the audit log under Settings > Audit Log.

Rotate team API tokens separately. OIDC trust policies are separate from personal API tokens. Engineers who left the team can have their tokens revoked without affecting any CI/CD pipeline that uses OIDC - the two auth paths are independent.

Review trust policies when repos are renamed or transferred. A trust policy scoped to oldorg/my-repo stops working if the repo moves to neworg/my-repo, but it also means a newly created repo at the old path can't accidentally inherit publish rights.

Fitting OIDC into a broader skill workflow

OIDC publishing works best as the final step in a skill review process. A practical workflow:

  1. Engineer updates skill files in a PR branch - new rules, updated conventions, whatever changed.
  2. Review happens in the PR - teammates can comment on the skill diff just like any other code change.
  3. PR merges to main - triggers the publish workflow via OIDC.
  4. Teams update their installations - with localskills pull or through automatic update notifications.

This is exactly how teams enforce AI coding standards at scale - the skill review process is as thorough as the code review process, and publishing is automatic once approved.

If you're new to structuring skills for team use, publishing your first skill covers the basics of the skill format, versioning, and visibility controls. For understanding why version-controlled AI rules matter, see AI rules and version control.

Summary

OIDC-based publishing removes the weakest link in automated skill pipelines: the long-lived API token. By replacing static secrets with short-lived identity tokens:

  • Nothing sensitive lives in your CI secret store
  • Trust policies enforce least-privilege: specific repos, specific branches, specific skills
  • Audit logs capture the full context of every publish
  • Token rotation is a non-issue - there's nothing to rotate

The setup is a few lines of YAML and a trust policy configuration. Once it's running, publishing updated AI skills becomes a zero-maintenance, automatic part of your standard merge workflow.


Ready to set up OIDC publishing for your team? Sign up for localskills.sh and configure your first trust policy today.

# Install the CLI
npm install -g @localskills/cli

# Log in (first time -- for local development)
localskills login

# Publish locally
localskills publish

# Or publish from CI via OIDC (no token needed)
localskills publish --oidc