Skip to main content
Copy-paste blueprints for common languages and use cases. Each template is self-contained. Combine them to build your full configuration. For a full breakdown of every field, see the Blueprint reference.
Secrets: Templates reference secrets via $SECRET_NAME. Configure these in Settings > Secrets before using a template. Never hardcode credentials in your blueprint.

Quick start

Minimal blueprints for the most common setups. Copy one, paste it into the blueprint editor, and you’re done.
initialize: |
  npm install -g pnpm

maintenance: |
  pnpm install

knowledge:
  - name: lint
    contents: |
      Run `pnpm lint` to check for errors.
  - name: test
    contents: |
      Run `pnpm test` for the full suite.
initialize: |
  curl -LsSf https://astral.sh/uv/install.sh | sh

maintenance: |
  uv sync

knowledge:
  - name: lint
    contents: |
      Run `uv run ruff check .` to lint.
  - name: test
    contents: |
      Run `uv run pytest` for the full suite.
initialize:
  - name: Install pnpm
    run: npm install -g pnpm
  - name: Install uv
    run: curl -LsSf https://astral.sh/uv/install.sh | sh

maintenance:
  - name: Frontend deps
    run: (cd frontend && pnpm install)
  - name: Backend deps
    run: (cd backend && uv sync)

knowledge:
  - name: structure
    contents: |
      - `frontend/` — React app (pnpm)
      - `backend/` — Python API (uv)
  - name: test
    contents: |
      Frontend: cd frontend && pnpm test
      Backend: cd backend && uv run pytest

Repository blueprints

Per-repo build steps, dependency management, and knowledge entries. Set these in Settings > Environment configuration > [your repo].

Python

Node.js

Standard Node.js setup with npm.
initialize: |
  nvm install 20
  nvm use 20

maintenance: |
  npm install

knowledge:
  - name: lint
    contents: |
      npx eslint .

  - name: test
    contents: |
      npm test

  - name: build
    contents: |
      npm run build
Use npm install (not npm ci) in maintenance. It does an incremental update, while npm ci deletes node_modules and reinstalls from scratch every session.

Go

Standard Go setup with modules.
initialize: |
  GO_VERSION=1.23.5
  ARCH=$(dpkg --print-architecture)
  curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
    | sudo tar -xz -C /usr/local
  echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
    | sudo tee /etc/profile.d/golang.sh > /dev/null

  go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

maintenance: |
  go mod download

knowledge:
  - name: lint
    contents: |
      golangci-lint run

  - name: test
    contents: |
      go test ./...

  - name: build
    contents: |
      go build ./...

Java

Java setup with Gradle.
JDK 17 is pre-installed on Devin’s base image. Skip the JDK install step if the default OpenJDK 17 is sufficient.
initialize:
  - name: Install JDK 17
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
      echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
        | sudo tee /etc/profile.d/java.sh > /dev/null

  - name: Install Gradle
    run: |
      GRADLE_VERSION=8.12
      curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
        -o /tmp/gradle.zip
      sudo unzip -qo /tmp/gradle.zip -d /opt
      sudo ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
      rm /tmp/gradle.zip

maintenance: |
  ./gradlew dependencies

knowledge:
  - name: lint
    contents: |
      ./gradlew check

  - name: test
    contents: |
      ./gradlew test

  - name: build
    contents: |
      ./gradlew build

Ruby on Rails

Rails setup with PostgreSQL.
initialize:
  - name: Install Ruby 3.3
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full libpq-dev postgresql-client

maintenance: |
  bundle install
  rails db:migrate

knowledge:
  - name: lint
    contents: |
      bundle exec rubocop

  - name: test
    contents: |
      bundle exec rspec

  - name: build
    contents: |
      rails assets:precompile

Rust

Standard Rust setup with cargo.
Rust (via rustup) and Cargo are pre-installed on Devin’s base image. Skip the install step if the default stable toolchain is sufficient. You only need the dependency fetch.
initialize: |
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
  source ~/.cargo/env

maintenance: |
  cargo fetch

knowledge:
  - name: lint
    contents: |
      cargo clippy -- -D warnings

  - name: test
    contents: |
      cargo test

  - name: build
    contents: |
      cargo build --release

Monorepos

Monorepo with a Node.js frontend and Python backend. Each subproject gets its own knowledge entries.
initialize:
  - name: Install pnpm
    run: npm install -g pnpm
  - name: Install uv
    run: curl -LsSf https://astral.sh/uv/install.sh | sh

maintenance:
  - name: Install frontend dependencies
    run: (cd packages/frontend && pnpm install)
  - name: Install backend dependencies
    run: (cd packages/backend && uv sync)
  - name: Build shared library
    run: (cd packages/shared && pnpm install && pnpm build)

knowledge:
  - name: structure
    contents: |
      This is a monorepo with three packages:
      - `packages/frontend` — React app (TypeScript, pnpm)
      - `packages/backend` — Python API (FastAPI, uv)
      - `packages/shared` — Shared TypeScript utilities (must be built before frontend)
  - name: frontend
    contents: |
      Run `cd packages/frontend && pnpm dev` to start the dev server.
      Run `cd packages/frontend && pnpm lint` to lint.
      Run `cd packages/frontend && pnpm test` to test.
  - name: backend
    contents: |
      Run `cd packages/backend && uv run uvicorn app.main:app --reload` to start the API.
      Run `cd packages/backend && uv run ruff check .` to lint.
      Run `cd packages/backend && uv run pytest` to test.
Use subshells (cd dir && command) instead of cd dir && command so the working directory resets between steps.

Private package registries

Configure package managers to resolve dependencies from private registries. Set these in Settings > Environment configuration > Org-wide setup (or per-repo if only one repo needs it).
Credential configuration belongs in maintenance, not initialize. Steps that write secrets (registry passwords, auth tokens) into config files should use maintenance so credentials are freshly loaded each session. Secrets are removed before the snapshot is saved, so config files written during initialize won’t have valid credentials when sessions start.
If your private registry uses a corporate CA, make sure the CA certificate is installed at the enterprise level first. The configuration below assumes HTTPS trust is already established.

Node.js registries

Configure npm to resolve scoped packages (e.g., @myorg/*) from a private registry, while public packages continue to come from the default npm registry.
  • GITHUB_PACKAGES_TOKEN — Personal access token or GitHub App token with read:packages scope
maintenance:
  - name: Configure npm scoped registry
    run: |
      npm config set @myorg:registry https://npm.pkg.github.com
      npm config set //npm.pkg.github.com/:_authToken $GITHUB_PACKAGES_TOKEN
Replace @myorg with your npm scope. Common private registry URLs:
  • GitHub Packages: https://npm.pkg.github.com
  • Artifactory: https://artifactory.example.com/artifactory/api/npm/npm-virtual
  • Nexus: https://nexus.example.com/repository/npm-group
  • GitLab: https://gitlab.example.com/api/v4/packages/npm
  • AWS CodeArtifact: https://<domain>.d.codeartifact.<region>.amazonaws.com/npm/<repo>

Python registries

Configure pip and uv to resolve packages from your private PyPI registry (e.g., Nexus, Artifactory).
  • PYPI_REGISTRY_URL — Full URL of your PyPI index, including credentials if required (e.g., https://user:token@nexus.example.com/repository/pypi-proxy/simple)
maintenance:
  - name: Configure pip/uv for private registry
    run: |
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = $PYPI_REGISTRY_URL
      EOF

      echo "export UV_INDEX_URL=$PYPI_REGISTRY_URL" \
        | sudo tee /etc/profile.d/uv-registry.sh > /dev/null
Common PyPI registry URL patterns:
  • Artifactory: https://artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple
  • Nexus: https://nexus.example.com/repository/pypi-proxy/simple
  • AWS CodeArtifact: https://aws:TOKEN@domain-owner.d.codeartifact.region.amazonaws.com/pypi/repo/simple/
  • Azure Artifacts: https://pkgs.dev.azure.com/org/project/_packaging/feed/pypi/simple
  • GitLab: https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/pypi/simple

JVM registries

Install JDK and configure Maven to mirror all dependency resolution through your private registry (e.g., Artifactory, Nexus).
JDK 17 is pre-installed on Devin’s base image. Skip the install step if the default OpenJDK 17 is sufficient. You only need the Maven install and registry configuration.
  • MAVEN_REGISTRY_URL — URL of your Maven registry (e.g., https://artifactory.example.com/artifactory/maven-virtual)
  • REGISTRY_USER — Registry username
  • REGISTRY_PASS — Registry password or API token
initialize:
  - name: Install JDK 17
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
      echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
        | sudo tee /etc/profile.d/java.sh > /dev/null

  - name: Install Maven
    run: |
      MAVEN_VERSION=3.9.9
      curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
        | sudo tar -xz -C /opt
      sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

maintenance:
  - name: Configure Maven for private registry
    run: |
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <mirrors>
          <mirror>
            <id>private-registry</id>
            <mirrorOf>*</mirrorOf>
            <url>$MAVEN_REGISTRY_URL</url>
          </mirror>
        </mirrors>
        <servers>
          <server>
            <id>private-registry</id>
            <username>$REGISTRY_USER</username>
            <password>$REGISTRY_PASS</password>
          </server>
        </servers>
      </settings>
      EOF
Common registry URL patterns for Maven:
  • Artifactory: https://artifactory.example.com/artifactory/maven-virtual
  • Nexus: https://nexus.example.com/repository/maven-public
  • Azure Artifacts: https://pkgs.dev.azure.com/org/project/_packaging/feed/maven/v1
  • GitHub Packages: https://maven.pkg.github.com
  • GitLab: https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/maven
  • AWS CodeArtifact: https://<domain>.d.codeartifact.<region>.amazonaws.com/maven/<repo>

Other registries

Install Go and configure it to resolve modules through a private module proxy (e.g., Athens, Artifactory, or a GOPROXY endpoint).
  • GO_PROXY_URL — URL of your Go module proxy (e.g., https://athens.corp.internal)
  • GIT_TOKEN — Personal access token for private Git repos that host Go modules
initialize:
  - name: Install Go
    run: |
      GO_VERSION=1.23.5
      ARCH=$(dpkg --print-architecture)
      curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
        | sudo tar -xz -C /usr/local
      echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
        | sudo tee /etc/profile.d/golang.sh > /dev/null

  - name: Configure Go for private modules
    run: |
      cat << 'GOENV' | sudo tee /etc/profile.d/go-private.sh > /dev/null
      export GOPROXY="$GO_PROXY_URL,direct"
      export GONOSUMCHECK="corp.internal/*,github.com/myorg/*"
      export GOPRIVATE="corp.internal/*,github.com/myorg/*"
      GOENV

maintenance:
  - name: Authenticate Go private modules
    run: |
      git config --global url."https://$GIT_TOKEN@github.com/myorg/".insteadOf "https://github.com/myorg/"
Common Go proxy URL patterns:
  • Artifactory: https://artifactory.example.com/artifactory/go-virtual
  • Nexus: https://nexus.example.com/repository/go-proxy
  • Athens: https://athens.corp.internal
Configure NuGet to resolve packages from a private feed.
  • NUGET_SOURCE_URL — URL of your NuGet feed
  • NUGET_API_KEY — API key or PAT for the feed
initialize:
  - name: Install .NET SDK
    run: |
      curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
      echo 'export PATH="$HOME/.dotnet:$PATH"' \
        | sudo tee /etc/profile.d/dotnet.sh > /dev/null

maintenance:
  - name: Configure NuGet for private feed
    run: |
      dotnet nuget add source "$NUGET_SOURCE_URL" \
        --name private \
        --username any \
        --password "$NUGET_API_KEY" \
        --store-password-in-clear-text 2>/dev/null || \
      dotnet nuget update source private \
        --source "$NUGET_SOURCE_URL" \
        --username any \
        --password "$NUGET_API_KEY" \
        --store-password-in-clear-text
Configure Docker to pull from a private container registry.
  • DOCKER_MIRROR_URL (optional) — URL of your Docker Hub mirror (e.g., https://mirror.corp.internal)
  • DOCKER_REGISTRY_URL — URL of your private container registry (e.g., registry.corp.internal:5000)
  • DOCKER_REGISTRY_USER — Registry username
  • DOCKER_REGISTRY_PASS — Registry password or API token
initialize:
  - name: Create Docker config directory
    run: sudo mkdir -p /etc/docker

maintenance:
  - name: Configure Docker for private registry
    run: |
      # Configure registry mirror (optional — routes Docker Hub pulls through your registry)
      cat << EOF | sudo tee /etc/docker/daemon.json > /dev/null
      {
        "registry-mirrors": ["$DOCKER_MIRROR_URL"]
      }
      EOF
      sudo systemctl restart docker || true

      # Log in to the private container registry
      echo "$DOCKER_REGISTRY_PASS" | docker login "$DOCKER_REGISTRY_URL" \
        --username "$DOCKER_REGISTRY_USER" \
        --password-stdin
Common container registry URLs:
  • Amazon ECR: <account-id>.dkr.ecr.<region>.amazonaws.com
  • Azure Container Registry: <name>.azurecr.io
  • Google Artifact Registry: <region>-docker.pkg.dev
  • GitHub Container Registry: ghcr.io
  • GitLab Container Registry: registry.gitlab.example.com
  • Nexus: https://nexus.example.com:8443
  • JFrog: <name>.jfrog.io
Configure Cargo to resolve crates from a private registry.
Rust (via rustup) and Cargo are pre-installed on Devin’s base image. Skip the install step if the default stable toolchain is sufficient. You only need the registry configuration.
  • CARGO_REGISTRY_INDEX — URL of the private registry index (e.g., sparse+https://cargo.corp.internal/api/v1/crates/)
  • CARGO_REGISTRY_TOKEN — Auth token for the private registry
initialize:
  - name: Install Rust
    run: |
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
        | sh -s -- -y --default-toolchain stable
      echo 'source "$HOME/.cargo/env"' \
        | sudo tee /etc/profile.d/rust.sh > /dev/null

maintenance:
  - name: Configure Cargo for private registry
    run: |
      mkdir -p ~/.cargo
      cat > ~/.cargo/config.toml << EOF
      [registries.private]
      index = "$CARGO_REGISTRY_INDEX"
      token = "$CARGO_REGISTRY_TOKEN"

      [source.crates-io]
      replace-with = "private"

      [source.private]
      registry = "$CARGO_REGISTRY_INDEX"
      EOF
If you only need to add a private registry without replacing crates.io, remove the [source.crates-io] and [source.private] sections and use cargo install --registry private or [dependencies] my-crate = { version = "1.0", registry = "private" } in Cargo.toml.
Install Ruby and configure Bundler to resolve gems from a private gem server.
  • GEM_SERVER_URL — URL of your private gem server (e.g., https://artifactory.example.com/artifactory/api/gems/gems-virtual)
  • REGISTRY_USER — Registry username
  • REGISTRY_PASS — Registry password or API token
initialize:
  - name: Install Ruby
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full

maintenance:
  - name: Configure Bundler for private gem server
    run: |
      bundle config set mirror.https://rubygems.org "$GEM_SERVER_URL"
      bundle config set "$GEM_SERVER_URL" "$REGISTRY_USER:$REGISTRY_PASS"
Common gem server URL patterns:
  • Artifactory: https://artifactory.example.com/artifactory/api/gems/gems-virtual
  • Nexus: https://nexus.example.com/repository/rubygems-proxy
  • Gemfury: https://gem.fury.io/<org>
Install PHP and configure Composer to resolve packages from a private Packagist or Satis registry.
  • COMPOSER_REGISTRY_URL — URL of your private Composer registry (e.g., https://repo.packagist.com/<org>)
  • REGISTRY_USER — Registry username
  • REGISTRY_PASS — Registry password or API token
initialize:
  - name: Install PHP and Composer
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
        php-cli php-mbstring php-xml php-curl unzip

      # Install Composer
      curl -sS https://getcomposer.org/installer | php
      sudo mv composer.phar /usr/local/bin/composer

maintenance:
  - name: Configure Composer for private registry
    run: |
      composer config --global repositories.private \
        composer "$COMPOSER_REGISTRY_URL"

      # Authenticate with the registry
      composer config --global http-basic.$(echo "$COMPOSER_REGISTRY_URL" \
        | sed 's|https\?://||;s|/.*||') "$REGISTRY_USER" "$REGISTRY_PASS"
Common Composer registry URL patterns:
  • Artifactory: https://artifactory.example.com/artifactory/api/composer/packagist-virtual
  • Nexus: https://nexus.example.com/repository/packagist-proxy
  • Private Packagist: https://repo.packagist.com/<org>
  • Satis: https://satis.corp.internal
AWS CodeArtifact tokens expire after 12 hours. Use maintenance to refresh the token at the start of every session. This example configures npm, pip, and Maven to use CodeArtifact.
awscli is pre-installed on Devin’s base image. You only need the token refresh and registry configuration.
  • AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY — IAM credentials with codeartifact:GetAuthorizationToken and sts:GetServiceBearerToken permissions
  • CA_DOMAIN — Your CodeArtifact domain name
  • CA_DOMAIN_OWNER — AWS account ID that owns the domain
  • CA_REGION — AWS region (e.g., us-east-1)
  • CA_NPM_REPO, CA_PYPI_REPO, CA_MAVEN_REPO — Repository names for each ecosystem
maintenance:
  - name: Refresh CodeArtifact auth token
    run: |
      # Get a fresh token (valid for 12 hours)
      export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
        --domain $CA_DOMAIN \
        --domain-owner $CA_DOMAIN_OWNER \
        --region $CA_REGION \
        --query authorizationToken \
        --output text)

      CA_ENDPOINT="https://${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com"

      # Configure npm
      npm config set registry "${CA_ENDPOINT}/npm/${CA_NPM_REPO}/"
      npm config set "//${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/npm/${CA_NPM_REPO}/:_authToken" "$CODEARTIFACT_AUTH_TOKEN"

      # Configure pip
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = https://aws:${CODEARTIFACT_AUTH_TOKEN}@${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/pypi/${CA_PYPI_REPO}/simple/
      EOF

      # Configure Maven (optional)
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <servers>
          <server>
            <id>codeartifact</id>
            <username>aws</username>
            <password>${CODEARTIFACT_AUTH_TOKEN}</password>
          </server>
        </servers>
        <mirrors>
          <mirror>
            <id>codeartifact</id>
            <mirrorOf>*</mirrorOf>
            <url>${CA_ENDPOINT}/maven/${CA_MAVEN_REPO}/</url>
          </mirror>
        </mirrors>
      </settings>
      EOF

Enterprise infrastructure

Machine-level infrastructure that applies across all orgs and repos. Set these in Settings > Devin’s base environment (for enterprise-wide) or Settings > Environment configuration > Org-wide setup (for org-wide).

Network and connectivity

Your organization uses a private certificate authority for internal services. Devin needs the root certificate to reach internal registries and tools over HTTPS.
  • CORP_ROOT_CA_B64 — Base64-encoded PEM certificate from your corporate CA. Generate with: cat corp-root-ca.crt | base64 -w0
initialize:
  - name: Install corporate CA certificate
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      sudo update-ca-certificates
      echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
        | sudo tee /etc/profile.d/node-ca.sh > /dev/null
If your organization uses multiple CA certificates (e.g., separate CAs for different internal services).
  • CORP_ROOT_CA_B64 — Base64-encoded primary CA certificate
  • CORP_INTERMEDIATE_CA_B64 — Base64-encoded intermediate CA certificate
initialize:
  - name: Install corporate CA certificates
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      echo "$CORP_INTERMEDIATE_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-intermediate-ca.crt > /dev/null
      sudo update-ca-certificates

      # Create a combined bundle for tools that need a single CA file
      cat /usr/local/share/ca-certificates/corp-*.crt \
        | sudo tee /usr/local/share/ca-certificates/corp-bundle.crt > /dev/null

      echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-bundle.crt' \
        | sudo tee /etc/profile.d/node-ca.sh > /dev/null
Route all network traffic through a corporate proxy.
  • CORP_HTTP_PROXY — HTTP proxy URL (e.g., http://proxy.corp.example.com:8080)
  • CORP_HTTPS_PROXY — HTTPS proxy URL
  • CORP_NO_PROXY — Comma-separated list of hosts to bypass proxy (e.g., localhost,127.0.0.1,.corp.example.com)
initialize:
  - name: Configure system-wide proxy
    run: |
      cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
      export http_proxy="$CORP_HTTP_PROXY"
      export https_proxy="$CORP_HTTPS_PROXY"
      export no_proxy="$CORP_NO_PROXY"
      export HTTP_PROXY="$CORP_HTTP_PROXY"
      export HTTPS_PROXY="$CORP_HTTPS_PROXY"
      export NO_PROXY="$CORP_NO_PROXY"
      PROXY
      source /etc/profile.d/proxy.sh

maintenance:
  - name: Configure git proxy
    run: |
      git config --global http.proxy "$CORP_HTTP_PROXY"
      git config --global https.proxy "$CORP_HTTPS_PROXY"

      # Configure npm proxy
      npm config set proxy "$CORP_HTTP_PROXY"
      npm config set https-proxy "$CORP_HTTPS_PROXY"
If your corporate proxy requires username/password authentication.
  • PROXY_USER — Proxy username
  • PROXY_PASS — Proxy password
  • PROXY_HOST — Proxy hostname and port (e.g., proxy.corp.example.com:8080)
  • CORP_NO_PROXY — Hosts to bypass proxy
initialize:
  - name: Configure authenticated proxy
    run: |
      PROXY_URL="http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}"

      cat << PROXY | sudo tee /etc/profile.d/proxy.sh > /dev/null
      export http_proxy="$PROXY_URL"
      export https_proxy="$PROXY_URL"
      export no_proxy="$CORP_NO_PROXY"
      export HTTP_PROXY="$PROXY_URL"
      export HTTPS_PROXY="$PROXY_URL"
      export NO_PROXY="$CORP_NO_PROXY"
      PROXY
      source /etc/profile.d/proxy.sh

maintenance:
  - name: Configure git for authenticated proxy
    run: |
      PROXY_URL="http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}"
      git config --global http.proxy "$PROXY_URL"
      git config --global https.proxy "$PROXY_URL"
Combined setup for environments that need both a corporate CA and a proxy. This is common in enterprise environments where internal services use private certificates and all traffic must route through a proxy.
  • CORP_ROOT_CA_B64 — Base64-encoded corporate CA certificate
  • CORP_HTTP_PROXY, CORP_HTTPS_PROXY — Proxy URLs
  • CORP_NO_PROXY — Hosts to bypass proxy
initialize:
  - name: Install corporate CA certificate
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      sudo update-ca-certificates
      echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
        | sudo tee /etc/profile.d/node-ca.sh > /dev/null

  - name: Configure system-wide proxy
    run: |
      cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
      export http_proxy="$CORP_HTTP_PROXY"
      export https_proxy="$CORP_HTTPS_PROXY"
      export no_proxy="$CORP_NO_PROXY"
      export HTTP_PROXY="$CORP_HTTP_PROXY"
      export HTTPS_PROXY="$CORP_HTTPS_PROXY"
      export NO_PROXY="$CORP_NO_PROXY"
      PROXY
      source /etc/profile.d/proxy.sh

maintenance:
  - name: Configure git proxy
    run: |
      git config --global http.proxy "$CORP_HTTP_PROXY"
      git config --global https.proxy "$CORP_HTTPS_PROXY"
Your private registries, Git servers, or other internal services are only reachable over VPN. This must run before other modules that need network access to internal resources.
OpenVPN:
  • VPN_CONFIG_B64 — Base64-encoded OpenVPN config file (.ovpn). Generate with: cat corp.ovpn | base64 -w0
  • VPN_AUTH_USER (optional) — VPN username, if your VPN requires username/password auth
  • VPN_AUTH_PASS (optional) — VPN password
WireGuard:
  • WG_CONFIG_B64 — Base64-encoded WireGuard config file. Generate with: cat wg0.conf | base64 -w0
OpenVPN:
initialize:
  - name: Install and configure OpenVPN
    run: |
      sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn

      # Write the VPN config
      sudo mkdir -p /etc/openvpn/client
      echo "$VPN_CONFIG_B64" | base64 -d \
        | sudo tee /etc/openvpn/client/corp.conf > /dev/null

      # If VPN requires username/password auth
      if [ -n "${VPN_AUTH_USER:-}" ] && [ -n "${VPN_AUTH_PASS:-}" ]; then
        printf '%s\n%s\n' "$VPN_AUTH_USER" "$VPN_AUTH_PASS" \
          | sudo tee /etc/openvpn/client/auth.txt > /dev/null
        sudo chmod 600 /etc/openvpn/client/auth.txt
        echo "auth-user-pass /etc/openvpn/client/auth.txt" \
          | sudo tee -a /etc/openvpn/client/corp.conf > /dev/null
      fi

      # Start the VPN tunnel
      sudo systemctl daemon-reload
      sudo systemctl enable --now openvpn-client@corp

      # Wait for the tunnel to come up
      for i in $(seq 1 30); do
        if ip link show tun0 >/dev/null 2>&1; then break; fi
        sleep 1
      done
WireGuard:
initialize:
  - name: Install and configure WireGuard
    run: |
      sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard-tools

      # Write the WireGuard config
      echo "$WG_CONFIG_B64" | base64 -d \
        | sudo tee /etc/wireguard/wg0.conf > /dev/null
      sudo chmod 600 /etc/wireguard/wg0.conf

      # Start the tunnel
      sudo systemctl enable --now wg-quick@wg0
For more details on VPN setup, see VPN Configuration.
Your internal services use private DNS names that aren’t resolvable by public DNS.
initialize:
  - name: Configure custom DNS resolution
    run: |
      # Add internal hostnames
      cat << 'HOSTS' | sudo tee -a /etc/hosts > /dev/null
      10.0.1.50  nexus.corp.internal
      10.0.1.51  git.corp.internal
      10.0.1.52  artifactory.corp.internal
      HOSTS

      # Optionally configure custom nameservers
      sudo mkdir -p /etc/systemd/resolved.conf.d
      cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
      [Resolve]
      DNS=10.0.0.53 10.0.0.54
      Domains=corp.internal
      DNS
      sudo systemctl restart systemd-resolved || true

Identity and security

Your organization requires all Git commits to be signed, and you want GitHub to mark Devin’s commits as Verified.
  • GPG_PRIVATE_KEY_B64 — Base64-encoded GPG private key. Generate with: gpg --export-secret-keys <key-id> | base64 -w0
  • GIT_USER_NAME — Git author name (e.g., Devin AI)
  • GIT_USER_EMAIL — Git author email. Must match a UID on the GPG key, otherwise GitHub will not verify the signature.
Also upload the matching public key to the GitHub account whose credentials Devin uses to push (under GitHub Settings > SSH and GPG keys). GitHub only marks commits as Verified when the signing public key is registered on the account that authored the commit.
initialize:
  - name: Prepare GPG and git signing config
    run: |
      # Allow GPG to work without a TTY
      echo 'export GPG_TTY=$(tty)' | sudo tee -a /etc/profile.d/gpg.sh > /dev/null

maintenance:
  - name: Import GPG key and configure git signing
    run: |
      echo "$GPG_PRIVATE_KEY_B64" | base64 -d | gpg --batch --import 2>/dev/null

      KEY_ID=$(gpg --list-secret-keys --keyid-format long 2>/dev/null \
        | grep sec | head -1 | awk '{print $2}' | cut -d'/' -f2)

      git config --global user.signingkey "$KEY_ID"
      git config --global commit.gpgsign true
      git config --global tag.gpgsign true
      git config --global gpg.program gpg
Configure Devin’s Git identity and SSH keys for accessing private Git servers.
  • GIT_USER_NAME — Git author name
  • GIT_USER_EMAIL — Git author email
  • SSH_PRIVATE_KEY_B64 — Base64-encoded SSH private key. Generate with: cat ~/.ssh/id_ed25519 | base64 -w0
  • SSH_KNOWN_HOSTS_B64 — Base64-encoded known hosts entries. Generate with: ssh-keyscan git.corp.internal | base64 -w0
  • SSH_CONFIG_B64 (optional) — Base64-encoded SSH config file
initialize:
  - name: Configure git identity
    run: |
      git config --global user.name "$GIT_USER_NAME"
      git config --global user.email "$GIT_USER_EMAIL"

      # Prepare SSH directory
      mkdir -p ~/.ssh && chmod 700 ~/.ssh

maintenance:
  - name: Install SSH keys
    run: |
      # Install SSH private key (in maintenance so it's freshly loaded each session)
      echo "$SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
      chmod 600 ~/.ssh/id_ed25519

      # Add known hosts for your Git server
      echo "$SSH_KNOWN_HOSTS_B64" | base64 -d >> ~/.ssh/known_hosts

      # Optionally install a custom SSH config
      if [ -n "${SSH_CONFIG_B64:-}" ]; then
        echo "$SSH_CONFIG_B64" | base64 -d > ~/.ssh/config
        chmod 600 ~/.ssh/config
      fi
Generate the known hosts entry for your Git server with ssh-keyscan git.corp.internal | base64 -w0.

System configuration

Install system-level packages that aren’t in the default Devin image (e.g., native libraries for image processing or PDF generation).
initialize:
  - name: Install system packages
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
        libpq-dev \
        libmagickwand-dev \
        poppler-utils \
        ffmpeg
Set persistent environment variables that should be available in every session.The recommended approach is to write KEY=VALUE lines to the $ENVRC file. Variables written to $ENVRC are automatically exported for all subsequent steps and the Devin session (similar to GitHub Actions’ $GITHUB_ENV).
initialize:
  - name: Set custom environment variables
    run: |
      echo "CORPORATE_ENV=production" >> $ENVRC
      echo "DEFAULT_REGION=us-east-1" >> $ENVRC
      echo "MAX_RETRIES=3" >> $ENVRC
You can also write env vars to /etc/profile.d/ scripts for system-wide availability:
cat << 'ENVVARS' | sudo tee /etc/profile.d/custom-env.sh > /dev/null
export CORPORATE_ENV=production
export DEFAULT_REGION=us-east-1
ENVVARS
Both approaches work. $ENVRC is simpler and recommended for most cases.
Default base images may have broken locale settings. Configure locale and timezone to prevent warnings from build tools, Java, Python, and Git.
initialize:
  - name: Configure locale and timezone
    run: |
      sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq locales

      # Generate and set the locale
      sudo sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
      sudo locale-gen
      sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

      cat << 'LOCALE' | sudo tee /etc/profile.d/locale.sh > /dev/null
      export LANG="en_US.UTF-8"
      export LC_ALL="en_US.UTF-8"
      LOCALE

      # Set timezone
      sudo timedatectl set-timezone UTC 2>/dev/null || \
        sudo ln -sfn /usr/share/zoneinfo/UTC /etc/localtime
Java, Gradle, and Node.js builds frequently hit the default 1024 open-file limit. Raise it to prevent build failures.
initialize:
  - name: Raise resource limits
    run: |
      cat << 'LIMITS' | sudo tee /etc/security/limits.d/99-devin.conf > /dev/null
      *    soft    nofile    65536
      *    hard    nofile    65536
      *    soft    nproc     65536
      *    hard    nproc     65536
      LIMITS

      # Also set the kernel max
      echo "fs.file-max = 65536" | sudo tee /etc/sysctl.d/99-devin-filemax.conf > /dev/null
      sudo sysctl -p /etc/sysctl.d/99-devin-filemax.conf 2>/dev/null || true
In air-gapped or restricted environments, replace the default Ubuntu APT sources with an internal mirror.
  • APT_MIRROR_URL — URL of your internal APT mirror (e.g., https://artifactory.example.com/artifactory/ubuntu-remote)
initialize:
  - name: Replace APT sources with internal mirror
    run: |
      # Backup original sources
      sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

      # Replace all Ubuntu mirrors with your internal mirror
      sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
      sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list

      sudo apt-get update -qq
Common APT mirror URL patterns:
  • Artifactory: https://artifactory.example.com/artifactory/ubuntu-remote
  • Nexus: https://nexus.example.com/repository/ubuntu-proxy

Advanced patterns

Devin’s base environment includes direnv. Use initialize to create .envrc files. Direnv loads them automatically.
initialize: |
  cat <<'EOF' > .envrc
  export DATABASE_URL=postgresql://localhost:5432/myapp_dev
  export REDIS_URL=redis://localhost:6379
  export APP_ENV=development
  EOF

maintenance: |
  direnv allow .
direnv is pre-hooked into Devin’s shell, so .envrc variables load automatically. No manual sourcing needed.
For sensitive environment variables (API keys, tokens, database passwords), use repo secrets instead of .envrc files. Repo secrets are stored securely and injected at session time. They never appear in your blueprint or snapshot.
Use nvm (pre-installed) to switch Node.js versions per repository via .nvmrc.
initialize: |
  nvm install 18
  nvm install 20
  nvm install 22

maintenance: |
  nvm use
nvm use reads .nvmrc from the repo root. Make sure your repository has one (e.g., containing 20).
Devin provides a Chrome browser with a CDP endpoint at localhost:29229 during sessions. Use Playwright scripts to automate browser-based login.
The browser is only available during sessions, not snapshot builds. Install Playwright in initialize and keep login scripts in your repo.
initialize: |
  pip install playwright
  playwright install chromium

maintenance: |
  npm install

knowledge:
  - name: browser-auth
    contents: |
      This project requires browser authentication.
      Run the login script before interacting with the app:
      python scripts/login.py

      Devin's Chrome browser is accessible via CDP at:
      http://localhost:29229
Example login script (scripts/login.py):
from playwright.sync_api import sync_playwright
import os

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://localhost:29229")
    context = browser.contexts[0]
    page = context.pages[0] if context.pages else context.new_page()

    page.goto("https://internal-tool.example.com/login")
    page.fill("#username", os.environ["TOOL_USERNAME"])
    page.fill("#password", os.environ["TOOL_PASSWORD"])
    page.click('button[type="submit"]')
    page.wait_for_url("**/dashboard")
Store login credentials as secrets, not in source code. For long-lived authentication, commit login scripts to .agents/skills/ so Devin can re-authenticate automatically.
Install system packages, custom binaries, and configure PATH in initialize.
initialize:
  - name: Install system packages
    run: |
      apt-get update
      apt-get install -y \
        jq \
        ripgrep \
        fd-find \
        protobuf-compiler \
        libssl-dev

  - name: Install custom CLI tool
    run: |
      curl -L https://github.com/example/tool/releases/download/v1.0/tool-linux-amd64 \
        -o /usr/local/bin/mytool
      chmod +x /usr/local/bin/mytool

  - name: Add custom bin directory to PATH
    run: |
      mkdir -p ~/bin
      echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc

knowledge:
  - name: tools
    contents: |
      Custom tools available:
      - mytool: installed at /usr/local/bin/mytool
      - Additional binaries can be placed in ~/bin
Devin supports running Node.js-based GitHub Actions directly in blueprints. This is useful for installing specific tool versions via the same actions your CI uses.
initialize:
  - name: "Install Node.js 20"
    uses: github.com/actions/setup-node@v4
    with:
      node-version: "20"

  - name: "Install Python 3.12"
    uses: github.com/actions/setup-python@v5
    with:
      python-version: "3.12"

  - name: "Install Go 1.22"
    uses: github.com/actions/setup-go@v5
    with:
      go-version: "1.22"

  - name: "Install Java 21"
    uses: github.com/actions/setup-java@v4
    with:
      java-version: "21"
      distribution: "temurin"

  - name: "Install Gradle"
    uses: github.com/gradle/actions/setup-gradle@v4

  - name: "Install Ruby 3.3"
    uses: github.com/ruby/setup-ruby@v1
    with:
      ruby-version: "3.3"

  - name: "Install additional tools"
    run: |
      pip install uv
      npm install -g pnpm turbo
      go install golang.org/x/tools/gopls@latest

maintenance: |
  echo "All runtimes are available:"
  node --version
  python --version
  go version
  java --version
Actions like setup-node and setup-python modify PATH and environment variables. Binaries installed by one action are available in all subsequent steps and in maintenance. Only Node.js-based GitHub Actions are supported. Composite and Docker-based actions are not.
You don’t need GitHub Actions for basic tool setup. Direct shell commands (nvm install 20, curl ... | sh, apt-get install) work just as well and are often simpler. GitHub Actions are most useful when you want to match your CI setup exactly or need the convenience of actions like setup-java which handle multiple distributions.

Full-stack examples

These examples show how enterprise and org-level configurations combine. In practice, you’d split these across scopes. They’re shown together here for reference.
A complete enterprise environment: corporate CA certificate, proxy, Java (Maven), Python (pip/uv), Node.js (npm), and Docker, all pointed at a single Artifactory instance.
Network & trust (account-wide):
  • CORP_ROOT_CA_B64 — Base64-encoded corporate CA certificate
  • CORP_HTTP_PROXY — HTTP proxy URL
  • CORP_HTTPS_PROXY — HTTPS proxy URL
  • CORP_NO_PROXY — Hosts to bypass proxy
Registry credentials (org-wide):
  • ARTIFACTORY_USER — Artifactory username
  • ARTIFACTORY_TOKEN — Artifactory API token or password
  • ARTIFACTORY_MAVEN_URL — Maven repository URL (e.g., https://artifactory.example.com/artifactory/maven-virtual)
  • ARTIFACTORY_PYPI_URL — PyPI repository URL (e.g., https://user:token@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple)
  • ARTIFACTORY_NPM_URL — npm repository URL (e.g., https://artifactory.example.com/artifactory/api/npm/npm-virtual)
  • ARTIFACTORY_DOCKER_URL — Docker registry URL (e.g., artifactory.example.com)
This would typically be split across three scopes:
  • Account-wide (initialize): Certificate and proxy
  • Org-wide (initialize): Language runtime installs
  • Org-wide (maintenance): Registry credentials (refreshed each session)
Shown here combined for reference:
initialize:
  # ── Account-wide: network and trust ──────────────────────────────────────

  - name: Install corporate CA certificate
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      sudo update-ca-certificates
      echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
        | sudo tee /etc/profile.d/node-ca.sh > /dev/null

  - name: Configure system-wide proxy
    run: |
      cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
      export http_proxy="$CORP_HTTP_PROXY"
      export https_proxy="$CORP_HTTPS_PROXY"
      export no_proxy="$CORP_NO_PROXY"
      export HTTP_PROXY="$CORP_HTTP_PROXY"
      export HTTPS_PROXY="$CORP_HTTPS_PROXY"
      export NO_PROXY="$CORP_NO_PROXY"
      PROXY
      source /etc/profile.d/proxy.sh

  # ── Org-wide: language runtimes ──────────────────────────────────────────

  - name: Install JDK 17 + Maven
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
      echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
        | sudo tee /etc/profile.d/java.sh > /dev/null

      MAVEN_VERSION=3.9.9
      curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
        | sudo tar -xz -C /opt
      sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

  - name: Install uv
    run: curl -LsSf https://astral.sh/uv/install.sh | sh

maintenance:
  # ── Account-wide: git proxy (refreshed each session) ───────────────────

  - name: Configure git proxy
    run: |
      git config --global http.proxy "$CORP_HTTP_PROXY"
      git config --global https.proxy "$CORP_HTTPS_PROXY"

  # ── Org-wide: registry credentials (refreshed each session) ──────────────

  - name: Configure Maven → Artifactory
    run: |
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <mirrors>
          <mirror>
            <id>artifactory</id>
            <mirrorOf>*</mirrorOf>
            <url>$ARTIFACTORY_MAVEN_URL</url>
          </mirror>
        </mirrors>
        <servers>
          <server>
            <id>artifactory</id>
            <username>$ARTIFACTORY_USER</username>
            <password>$ARTIFACTORY_TOKEN</password>
          </server>
        </servers>
      </settings>
      EOF

  - name: Configure pip/uv → Artifactory PyPI
    run: |
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = $ARTIFACTORY_PYPI_URL
      trusted-host = $(echo "$ARTIFACTORY_PYPI_URL" | sed 's|https\?://||;s|/.*||')
      EOF

      echo "export UV_INDEX_URL=$ARTIFACTORY_PYPI_URL" \
        | sudo tee /etc/profile.d/uv-registry.sh > /dev/null

  - name: Configure npm → Artifactory
    run: |
      npm config set registry "$ARTIFACTORY_NPM_URL"
      REGISTRY_HOST=$(echo "$ARTIFACTORY_NPM_URL" | sed 's|https\?://||;s|/.*||')
      npm config set "//${REGISTRY_HOST}/:_authToken" "$ARTIFACTORY_TOKEN"

  - name: Configure Docker → Artifactory
    run: |
      echo "$ARTIFACTORY_TOKEN" | docker login "$ARTIFACTORY_DOCKER_URL" \
        --username "$ARTIFACTORY_USER" \
        --password-stdin
In this example, all registries point at the same Artifactory instance but use different URL paths. Each package ecosystem has its own endpoint format. Maven, PyPI, npm, and Docker URLs are all different even for the same registry.
When different languages use different private registries (e.g., Maven from Nexus, npm from GitHub Packages, Python from Artifactory).
  • NEXUS_MAVEN_URL — Nexus Maven repository URL
  • NEXUS_USER — Nexus username
  • NEXUS_PASS — Nexus password
  • GITHUB_PACKAGES_TOKEN — GitHub personal access token with read:packages scope
  • ARTIFACTORY_USER — Artifactory username
  • ARTIFACTORY_TOKEN — Artifactory API token
  • GIT_TOKEN — Personal access token for Go private modules
maintenance:
  # Maven → Nexus
  - name: Configure Maven → Nexus
    run: |
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <mirrors>
          <mirror>
            <id>nexus</id>
            <mirrorOf>*</mirrorOf>
            <url>$NEXUS_MAVEN_URL</url>
          </mirror>
        </mirrors>
        <servers>
          <server>
            <id>nexus</id>
            <username>$NEXUS_USER</username>
            <password>$NEXUS_PASS</password>
          </server>
        </servers>
      </settings>
      EOF

  # npm → GitHub Packages (scoped)
  - name: Configure npm → GitHub Packages
    run: |
      npm config set @myorg:registry https://npm.pkg.github.com
      npm config set //npm.pkg.github.com/:_authToken $GITHUB_PACKAGES_TOKEN

  # Python → Artifactory
  - name: Configure pip → Artifactory
    run: |
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = https://$ARTIFACTORY_USER:$ARTIFACTORY_TOKEN@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple
      EOF

  # Go → private modules via git
  - name: Configure Go private modules
    run: |
      git config --global url."https://$GIT_TOKEN@github.com/myorg/".insteadOf "https://github.com/myorg/"
In a fully air-gapped environment, Devin cannot reach any public URLs. All tools, runtimes, and packages must come from internal mirrors.
Certificates:
  • CORP_ROOT_CA_B64 — Base64-encoded corporate CA certificate
Mirror access:
  • APT_MIRROR_URL — Internal Ubuntu APT mirror URL
  • MIRROR_USER — Mirror authentication username
  • MIRROR_PASS — Mirror authentication password
  • JDK_TARBALL_URL — URL to download JDK tarball from internal mirror
  • NODE_TARBALL_URL — URL to download Node.js tarball from internal mirror
Package registries:
  • INTERNAL_MAVEN_URL — Internal Maven registry URL
  • INTERNAL_NPM_URL — Internal npm registry URL
  • INTERNAL_PYPI_URL — Internal PyPI registry URL
initialize:
  - name: Install corporate CA certificate
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      sudo update-ca-certificates

  - name: Replace apt sources with internal mirror
    run: |
      sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
      sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
      sudo apt-get update -qq

  - name: Install JDK from internal mirror
    run: |
      # Download JDK tarball from internal artifact store
      curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$JDK_TARBALL_URL" \
        | sudo tar -xz -C /usr/local
      sudo ln -sf /usr/local/jdk-17.*/bin/java /usr/local/bin/java
      sudo ln -sf /usr/local/jdk-17.*/bin/javac /usr/local/bin/javac
      echo "export JAVA_HOME=$(ls -d /usr/local/jdk-17.*)" \
        | sudo tee /etc/profile.d/java.sh > /dev/null

  - name: Install Node.js from internal mirror
    run: |
      curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$NODE_TARBALL_URL" \
        | sudo tar -xz -C /usr/local --strip-components=1

maintenance:
  - name: Configure all package managers for internal registry
    run: |
      # Maven
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <mirrors>
          <mirror>
            <id>internal</id>
            <mirrorOf>*</mirrorOf>
            <url>$INTERNAL_MAVEN_URL</url>
          </mirror>
        </mirrors>
        <servers>
          <server>
            <id>internal</id>
            <username>$MIRROR_USER</username>
            <password>$MIRROR_PASS</password>
          </server>
        </servers>
      </settings>
      EOF

      # npm
      npm config set registry "$INTERNAL_NPM_URL"

      # pip
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = $INTERNAL_PYPI_URL
      EOF
In air-gapped environments, all tools Devin needs (language runtimes, CLI tools, etc.) must be available on your internal mirrors. Public registries and download sites are unreachable.
A comprehensive enterprise setup combining VPN connectivity with certificates, proxy, and multi-language support. This is the recommended order of operations.
VPN:
  • VPN_CONFIG_B64 — Base64-encoded OpenVPN config file
Network & trust:
  • CORP_ROOT_CA_B64 — Base64-encoded corporate CA certificate
  • CORP_HTTP_PROXY — HTTP proxy URL
  • CORP_HTTPS_PROXY — HTTPS proxy URL
  • CORP_NO_PROXY — Hosts to bypass proxy
Registry credentials:
  • MAVEN_REGISTRY_URL — Maven registry URL
  • NPM_REGISTRY_URL — npm registry URL
  • PYPI_REGISTRY_HOST — PyPI registry hostname
  • REGISTRY_USER — Registry username (for Maven and pip)
  • REGISTRY_PASS — Registry password (for Maven and pip)
  • REGISTRY_TOKEN — npm auth token
initialize:
  # 1. VPN — must come first so internal resources are reachable
  - name: Establish VPN connection
    run: |
      sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn
      sudo mkdir -p /etc/openvpn/client
      echo "$VPN_CONFIG_B64" | base64 -d \
        | sudo tee /etc/openvpn/client/corp.conf > /dev/null
      sudo systemctl daemon-reload
      sudo systemctl enable --now openvpn-client@corp
      for i in $(seq 1 30); do
        if ip link show tun0 >/dev/null 2>&1; then break; fi
        sleep 1
      done

  # 2. DNS — resolve internal hostnames
  - name: Configure DNS
    run: |
      sudo mkdir -p /etc/systemd/resolved.conf.d
      cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
      [Resolve]
      DNS=10.0.0.53
      Domains=corp.internal
      DNS
      sudo systemctl restart systemd-resolved || true

  # 3. Certificates — trust internal CAs
  - name: Install CA certificate
    run: |
      echo "$CORP_ROOT_CA_B64" | base64 -d \
        | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
      sudo update-ca-certificates
      echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
        | sudo tee /etc/profile.d/node-ca.sh > /dev/null

  # 4. Proxy — route traffic through corporate proxy
  - name: Configure proxy
    run: |
      cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
      export http_proxy="$CORP_HTTP_PROXY"
      export https_proxy="$CORP_HTTPS_PROXY"
      export no_proxy="$CORP_NO_PROXY"
      export HTTP_PROXY="$CORP_HTTP_PROXY"
      export HTTPS_PROXY="$CORP_HTTPS_PROXY"
      export NO_PROXY="$CORP_NO_PROXY"
      PROXY
      source /etc/profile.d/proxy.sh

  # 5. Language runtimes
  - name: Install JDK 17
    run: |
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
      echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
        | sudo tee /etc/profile.d/java.sh > /dev/null

  - name: Install Node.js tooling
    run: npm install -g pnpm

  - name: Install uv
    run: curl -LsSf https://astral.sh/uv/install.sh | sh

maintenance:
  - name: Configure git proxy
    run: |
      git config --global http.proxy "$CORP_HTTP_PROXY"
      git config --global https.proxy "$CORP_HTTPS_PROXY"

  - name: Configure Maven
    run: |
      mkdir -p ~/.m2
      cat > ~/.m2/settings.xml << EOF
      <settings>
        <mirrors>
          <mirror>
            <id>corp</id>
            <mirrorOf>*</mirrorOf>
            <url>$MAVEN_REGISTRY_URL</url>
          </mirror>
        </mirrors>
        <servers>
          <server>
            <id>corp</id>
            <username>$REGISTRY_USER</username>
            <password>$REGISTRY_PASS</password>
          </server>
        </servers>
      </settings>
      EOF

  - name: Configure npm
    run: |
      npm config set registry "$NPM_REGISTRY_URL"
      NPM_HOST=$(echo "$NPM_REGISTRY_URL" | sed 's|https\?://||;s|/.*||')
      npm config set "//${NPM_HOST}/:_authToken" "$REGISTRY_TOKEN"

  - name: Configure pip/uv
    run: |
      mkdir -p ~/.config/pip
      cat > ~/.config/pip/pip.conf << EOF
      [global]
      index-url = https://$REGISTRY_USER:$REGISTRY_PASS@${PYPI_REGISTRY_HOST}/simple
      EOF
      echo "export UV_INDEX_URL=https://$REGISTRY_USER:$REGISTRY_PASS@${PYPI_REGISTRY_HOST}/simple" \
        | sudo tee /etc/profile.d/uv-registry.sh > /dev/null
Order matters for initialize steps. VPN must come first (so internal hosts are reachable), then DNS (so names resolve), then certificates (so HTTPS works), then proxy (so traffic routes correctly), and finally language runtimes (which may download from internal mirrors).

Tips for writing good blueprints

  • Test commands in a session first. Run commands manually in a Devin session before adding them to your blueprint. This is faster than waiting for a full build cycle.
  • Use initialize for install-once tools, maintenance for deps. Anything that takes minutes to install (compilers, large binaries, global tools) belongs in initialize. Quick dependency commands (npm install, uv sync) go in maintenance.
  • Keep maintenance commands fast. Aim for under 2 minutes. These run at the start of every session.
  • Use $ENVRC for environment variables. Don’t write to .bashrc or .profile. $ENVRC is the supported mechanism for setting variables across steps and sessions.
  • Name your steps. The expanded form with name fields makes build log failures much easier to identify.
  • Use subshells for monorepos. (cd packages/foo && npm install) runs in a subshell so subsequent steps aren’t affected by the directory change.
  • Use npm install, not npm ci. npm ci deletes node_modules and reinstalls from scratch every session, which is slow for maintenance.
  • Use repo secrets for sensitive values. Configure them in Settings > Secrets with repo scope instead of hardcoding in blueprints.
For syntax details, see the Blueprint reference. For troubleshooting build failures, see Declarative configuration > Troubleshooting.