diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index b8f2c65..42981ee 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -17,7 +17,9 @@
         "redhat.vscode-yaml",
         "streetsidesoftware.code-spell-checker",
         "editorconfig.editorconfig",
-        "ms-azuretools.vscode-docker"
+        "ms-azuretools.vscode-docker",
+        "timonwong.shellcheck",
+        "mkhl.shfmt"
       ]
     }
   },
@@ -27,6 +29,5 @@
   },
   "remoteUser": "node",
   "onCreateCommand": "./.devcontainer/onCreateCommand.sh",
-  "postAttachCommand": "pre-commit install",
-  "updateContentCommand": "npm install -g @devcontainers/cli"
+  "postAttachCommand": "pre-commit install"
 }
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index bef5fee..ee45f3c 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -29,10 +29,9 @@ jobs:
       matrix:
         features:
           - aws-ssm-session-manager-plugin
+          - openapi-generator-cli
         baseImage:
           - debian:latest
-          - ubuntu:latest
-          - mcr.microsoft.com/devcontainers/base:ubuntu
     steps:
       - uses: actions/checkout@v4
 
@@ -48,6 +47,7 @@ jobs:
       matrix:
         features:
           - aws-ssm-session-manager-plugin
+          - openapi-generator-cli
     steps:
       - uses: actions/checkout@v4
 
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 57572c7..d0ed9ca 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -19,7 +19,7 @@ jobs:
           publish-features: "true"
           base-path-to-features: "./src"
           generate-docs: "true"
-          
+
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e695a5a..6551f81 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,3 +6,8 @@ repos:
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
+
+  - repo: https://github.com/shellcheck-py/shellcheck-py
+    rev: v0.10.0.1
+    hooks:
+      - id: shellcheck
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 48f223f..248eddd 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,6 +4,8 @@
         "redhat.vscode-yaml",
         "streetsidesoftware.code-spell-checker",
         "editorconfig.editorconfig",
-        "ms-azuretools.vscode-docker"
+        "ms-azuretools.vscode-docker",
+        "timonwong.shellcheck",
+        "mkhl.shfmt"
     ]
 }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f4a8e5a..a987aae 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,8 @@
 {
     "cSpell.words": [
-        "lasuillard"
+        "devcontainers",
+        "lasuillard",
+        "openapi"
     ],
     "editor.codeActionsOnSave": {
         "source.fixAll": "explicit",
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 27aca03..53743c8 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -9,4 +9,4 @@
             "problemMatcher": []
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..14a3b54
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,54 @@
+#!/usr/bin/env -S make -f
+
+MAKEFLAGS += --warn-undefined-variable
+MAKEFLAGS += --no-builtin-rules
+MAKEFLAGS += --silent
+
+SHELL := bash
+.ONESHELL:
+.SHELLFLAGS := -eu -o pipefail -c
+.DELETE_ON_ERROR:
+.DEFAULT_GOAL := help
+
+help: Makefile  ## Show help
+	@grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
+
+
+# =============================================================================
+# Common
+# =============================================================================
+install:  ## Install deps
+	npm install -g @devcontainers/cli
+	pre-commit install --install-hooks
+.PHONY: install
+
+update:  ## Update deps and tools
+	pre-commit autoupdate
+.PHONY: update
+
+
+# =============================================================================
+# CI
+# =============================================================================
+ci: lint test  ## Run CI tasks
+.PHONY: ci
+
+format:  ## Run autoformatters
+
+.PHONY: format
+
+lint:  ## Run all linters
+
+.PHONY: lint
+
+test:  ## Run tests
+	devcontainer features test
+.PHONY: test
+
+
+# =============================================================================
+# Handy Scripts
+# =============================================================================
+clean:  ## Remove temporary files
+
+.PHONY: clean
diff --git a/src/aws-ssm-session-manager-plugin/devcontainer-feature.json b/src/aws-ssm-session-manager-plugin/devcontainer-feature.json
index 487cb10..4abdb9d 100644
--- a/src/aws-ssm-session-manager-plugin/devcontainer-feature.json
+++ b/src/aws-ssm-session-manager-plugin/devcontainer-feature.json
@@ -3,4 +3,4 @@
     "id": "aws-ssm-session-manager-plugin",
     "version": "0.1.0",
     "description": "A feature to install AWS SSM Session Manager plugin."
-} 
+}
diff --git a/src/aws-ssm-session-manager-plugin/install.sh b/src/aws-ssm-session-manager-plugin/install.sh
index 0058822..2f09121 100644
--- a/src/aws-ssm-session-manager-plugin/install.sh
+++ b/src/aws-ssm-session-manager-plugin/install.sh
@@ -4,6 +4,8 @@ set -e
 
 ARCH=$(uname -m)
 
+echo "Preparing to download plugin for $ARCH"
+
 case $ARCH in
     x86_64)
         download_url='https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb'
@@ -20,8 +22,13 @@ case $ARCH in
         ;;
 esac
 
+echo "Downloading plugin from $download_url"
+
 apt-get update && apt-get install -y curl
 
 curl -sL "$download_url" -o /tmp/session-manager-plugin.deb
+
+echo "Installing plugin"
+
 dpkg -i /tmp/session-manager-plugin.deb
 rm /tmp/session-manager-plugin.deb
diff --git a/src/openapi-generator-cli/README.md b/src/openapi-generator-cli/README.md
new file mode 100644
index 0000000..a1e7d1b
--- /dev/null
+++ b/src/openapi-generator-cli/README.md
@@ -0,0 +1,26 @@
+
+# My Favorite Color (color)
+
+A feature to remind you of your favorite color
+
+## Example Usage
+
+```json
+"features": {
+    "ghcr.io/devcontainers/feature-starter/color:1": {
+        "version": "latest"
+    }
+}
+```
+
+## Options
+
+| Options Id | Description | Type | Default Value |
+|-----|-----|-----|-----|
+| favorite | Choose your favorite color. | string | red |
+
+
+
+---
+
+_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/devcontainers/feature-starter/blob/main/src/color/devcontainer-feature.json).  Add additional notes to a `NOTES.md`._
diff --git a/src/openapi-generator-cli/devcontainer-feature.json b/src/openapi-generator-cli/devcontainer-feature.json
new file mode 100644
index 0000000..0a55c4c
--- /dev/null
+++ b/src/openapi-generator-cli/devcontainer-feature.json
@@ -0,0 +1,16 @@
+{
+  "name": "OpenAPI Generator CLI",
+  "id": "openapi-generator-cli",
+  "version": "0.1.0",
+  "description": "A feature to install OpenAPI Generator CLI. Java is required but not included in this feature.",
+  "options": {
+    "version": {
+      "type": "string",
+      "default": "latest",
+      "description": "The version of OpenAPI Generator CLI to install."
+    }
+  },
+  "installsAfter": [
+    "ghcr.io/devcontainers/features/java"
+  ]
+}
diff --git a/src/openapi-generator-cli/install.sh b/src/openapi-generator-cli/install.sh
new file mode 100644
index 0000000..fed333b
--- /dev/null
+++ b/src/openapi-generator-cli/install.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+set -e
+
+apt-get update && apt-get install -y curl
+
+if [ "$VERSION" = 'latest' ]; then
+    echo "Resolving latest version of OpenAPI Generator CLI"
+    VERSION=$(
+        curl https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/maven-metadata.xml \
+            | grep -Eo '<latest>(.+)</latest>' \
+            | sed -E 's/<latest>(.+)<\/latest>/\1/'
+    )
+fi
+
+echo "Installing OpenAPI Generator CLI with version ${VERSION}"
+
+curl -o /usr/local/lib/openapi-generator-cli.jar \
+    "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${VERSION}/openapi-generator-cli-${VERSION}.jar"
+
+echo "Creating wrapper script for OpenAPI Generator CLI"
+
+cat <<EOF > /usr/local/bin/openapi-generator-cli
+#!/bin/sh
+
+java -jar /usr/local/lib/openapi-generator-cli.jar "\$@"
+EOF
+chmod +x /usr/local/bin/openapi-generator-cli
diff --git a/test/_global/scenarios.json b/test/_global/scenarios.json
index 9e26dfe..0967ef4 100644
--- a/test/_global/scenarios.json
+++ b/test/_global/scenarios.json
@@ -1 +1 @@
-{}
\ No newline at end of file
+{}
diff --git a/test/aws-ssm-session-manager-plugin/test.sh b/test/aws-ssm-session-manager-plugin/test.sh
index 8554f99..dcdc8c3 100644
--- a/test/aws-ssm-session-manager-plugin/test.sh
+++ b/test/aws-ssm-session-manager-plugin/test.sh
@@ -2,6 +2,7 @@
 
 set -e
 
+# shellcheck source=/dev/null
 source dev-container-features-test-lib
 
 check "verify session manager plugin installation" session-manager-plugin --version | grep -Eo '[0-9\.]+'
diff --git a/test/openapi-generator-cli/latest.sh b/test/openapi-generator-cli/latest.sh
new file mode 100644
index 0000000..e6314b6
--- /dev/null
+++ b/test/openapi-generator-cli/latest.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+# shellcheck source=/dev/null
+source dev-container-features-test-lib
+
+check "verify OpenAPI Generator CLI installation" openapi-generator-cli --version | grep -Eo '^openapi-generator-cli .+$'
+
+reportResults
diff --git a/test/openapi-generator-cli/scenarios.json b/test/openapi-generator-cli/scenarios.json
new file mode 100644
index 0000000..1cadca9
--- /dev/null
+++ b/test/openapi-generator-cli/scenarios.json
@@ -0,0 +1,18 @@
+{
+  "latest": {
+    "image": "mcr.microsoft.com/devcontainers/base:bookworm",
+    "features": {
+      "ghcr.io/devcontainers/features/java:1": {},
+      "openapi-generator-cli": {}
+    }
+  },
+  "specific-version": {
+    "image": "mcr.microsoft.com/devcontainers/base:bookworm",
+    "features": {
+      "ghcr.io/devcontainers/features/java:1": {},
+      "openapi-generator-cli": {
+        "version": "7.8.0"
+      }
+    }
+  }
+}
diff --git a/test/openapi-generator-cli/specific-version.sh b/test/openapi-generator-cli/specific-version.sh
new file mode 100644
index 0000000..2a4a47c
--- /dev/null
+++ b/test/openapi-generator-cli/specific-version.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+# shellcheck source=/dev/null
+source dev-container-features-test-lib
+
+check "verify OpenAPI Generator CLI installation" openapi-generator-cli --version | grep -Eo '^openapi-generator-cli 7\.8\.0$'
+
+reportResults
diff --git a/test/openapi-generator-cli/test.sh b/test/openapi-generator-cli/test.sh
new file mode 100644
index 0000000..16d38a3
--- /dev/null
+++ b/test/openapi-generator-cli/test.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+# shellcheck source=/dev/null
+source dev-container-features-test-lib
+
+check "can be installed even java not installed" openapi-generator-cli --version 2>&1 | grep 'java: not found'
+
+reportResults