TokenEconomizer
Experience-Driven Black-Box Cost Control for Coding Agents
A simple content management system
Translation pending
Developer toolsGitHub @olegivOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Repository information has not been collected yet.
No updates detected yet. GitHub releases will appear here.
Lightweight CMS built with Go and SQLite. Features HTMX/Alpine.js admin, REST API, theme & module systems, multi-language support, webhooks, media library, form builder and SEO tools. Single binary, zero config database.
Taken from the repository README
Loading the full README.
Read the full README ↗A single-binary Go CMS with HTMX/Alpine.js admin, REST API, themes & modules. SQLite by default — drop onto a server, run the rest.
Live demo: ocms-demo.fly.dev · Admin: /admin
Poke around, break things, see how it feels. The demo resets to a clean state every 24 hours (daily cron at 01:00 UTC, plus a catch-up reset on machine wake if Fly auto-stopped the instance during a quiet period). Worst-case staleness: 24h + the idle gap.
Hosted on Fly.io — see fly.toml and docs/demo-deployment.md for the full deployment recipe.
Most modern CMSs are PHP monoliths (WordPress, Drupal), Node-heavy with build steps (Strapi, Ghost), or fully headless and force you to build a frontend (Directus, Sanity). ocms-go is what falls out when you take Go's deployment model seriously and write the CMS as a single binary with an opinionated HTMX admin baked in:
scp it to a server, run it. No PHP-FPM, no Node toolchain, no Docker required.git clone https://github.com/olegiv/ocms-go.git && cd ocms-go
make install-hooks && make assets
OCMS_SESSION_SECRET=$(openssl rand -hex 32) OCMS_DO_SEED=true make dev
# → http://localhost:8080/admin [email protected] / changeme1234
Or with Docker:
OCMS_ENV=development OCMS_SESSION_SECRET=$(openssl rand -base64 32) \
OCMS_DO_SEED=true docker compose up -d
Source-build prerequisites — see Prerequisites below.
| Project | Lang | Single binary | Built-in admin | License | Active |
|---|---|---|---|---|---|
| ocms-go | Go | ✅ | ✅ HTMX | GPL-3.0 | ✅ |
| ponzu | Go | ✅ | ✅ | BSD-3 | last release 2020 |
| emarifer/goCMS | Go | ✅ | basic | MIT | ✅ |
| gouniverse/cms | Go | (library) | partial | AGPL-3 | ✅ |
| Strapi | Node | ❌ | ✅ | MIT | ✅ |
| Ghost | Node | ❌ | ✅ | MIT | ✅ |
| WordPress | PHP | ❌ | ✅ | GPL-2.0 | ✅ |
ocms-go targets the gap in the Go CMS ecosystem: actively developed, production-grade, single-binary, with a real admin UI.
Running ocms-go in production? Tell us about it.
The path to v1.0 is tracked in the Roadmap to v1.0 issue. Headlines:
Contributions welcome — see CONTRIBUTING.md and the Good first issues list.
.tar.gz HTML themes and removal of inactive custom themes without a restart; core themes are protected. See Custom Themes./api/mcpmodelcontextprotocol/go-sdk, protocol versions 2024-11-05 through 2026-07-28mcp:access permission and every API key policy appliespages:read/.well-known/mcp/server-card.json while activeocms mfa-reset for lost devices. See docs/login-security.md/account/securityper_page, current-page state in URL only)sort + dir)/ru/about-us)OCMS_REDIS_URL); shown in the cache admin and cleared with it, but the caches above stay per process, so instances do not share them/health endpoint for monitoringPrebuilt release binaries have no external runtime dependencies beyond the operating system. Set OCMS_SESSION_SECRET before starting the server; see Environment Variables.
Building from source requires:
Download the latest release archive for your OS and architecture:
curl -L https://github.com/olegiv/ocms-go/releases/latest/download/ocms-linux-amd64.tar.gz | tar -xz
./ocms -version
First run — scaffold a site and start it:
./ocms init my-site
cd my-site
../ocms serve # or put ocms on your PATH and run: ocms serve
ocms init creates my-site/ with a .env (including a freshly generated
OCMS_SESSION_SECRET and OCMS_MFA_KEY) plus data/, uploads/, and
custom/ directories.
ocms serve starts the server, reading ./.env from the current directory.
Running ocms with no command still starts the server, so existing
systemd/Docker setups are unaffected.
The server applies pending database migrations when it starts. To run the
core ones without starting it, use ocms migrate [-db <path>] up|down|status|version, which reads ./.env (or the file OCMS_ENV_FILE
names) like serve; module migrations still run when the server starts.
make migrate-up, migrate-down and migrate-status run it (add
DB_PATH=<path> for another database). make migrate-create still uses the
goose CLI, which does not know the Go migration and cannot roll it back.
Open http://localhost:8080/admin/ and log in with
[email protected] / changeme1234. Change the admin email and password
immediately. Stop the foreground process with Ctrl+C, set
OCMS_DO_SEED=false in my-site/.env, then run serve again for normal runs.
Keep OCMS_SESSION_SECRET stable; regenerating it invalidates active sessions.
Keep OCMS_MFA_KEY stable too, and back it up with the database: it seals the
authenticator-app secrets of two-factor authentication, and without it enrolled
users can sign in only with recovery codes. Someone who lost both their
authenticator and recovery codes is reset by an administrator on the user page,
or with ocms mfa-reset [-db <path>] <email>, which reads ./.env like
ocms migrate.
Do not set OCMS_ENV=production for this seeded bootstrap run. Production
startup rejects default seeded credentials.
Available release archives:
ocms-linux-amd64.tar.gzocms-linux-arm64.tar.gzocms-darwin-arm64.tar.gzEach release also ships checksums.txt, and every archive published after
v0.28.0 carries a signed build-provenance attestation that ties it to the
release workflow run, commit and tag. Verify a download before running it:
curl -LO https://github.com/olegiv/ocms-go/releases/latest/download/ocms-linux-amd64.tar.gz
gh attestation verify ocms-linux-amd64.tar.gz --repo olegiv/ocms-go
go install is not supported for the CMS binary because Go does not run the frontend asset pipeline before evaluating //go:embed.
Clone the repository:
git clone https://github.com/olegiv/ocms-go.git
cd ocms-go
Install dependencies:
go mod download
Install required tools:
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/a-h/templ/cmd/[email protected]
go install github.com/pressly/goose/v3/cmd/goose@latest
Generate code:
sqlc generate
templ generate
Build assets (installs npm dependencies and compiles SCSS):
make assets
Build the production binary:
make build-prod
| Variable | Description | Default | Required |
|---|---|---|---|
OCMS_SESSION_SECRET | Required server secret, validated at startup; nothing is keyed with it today: sessions are stored server-side and CSRF protection checks Fetch metadata headers (min 32 bytes; placeholder values from example files are refused, and the public test-suite secret is refused in production) | - | Yes |
OCMS_MFA_KEY | Seals the TOTP secrets of two-factor authentication at rest (min 32 bytes, distinct from OCMS_SESSION_SECRET; same placeholder and entropy checks). Unset: nobody can enroll an authenticator app, and enrolled users can sign in only with recovery codes. Back it up with the database | - | In production, unless OCMS_REQUIRE_MFA=false |
OCMS_REQUIRE_MFA | Every account (admin, editor and public) must pass a second factor at login; accounts without one set it up at their next sign-in, and sessions that started without one are signed out. Needs OCMS_MFA_KEY; ignored in demo mode (see docs/login-security.md) | false (true in production when unset) | No |
OCMS_DB_PATH | Path to SQLite database file | ./data/ocms.db | No |
OCMS_ENV_FILE | Env file to read instead of ./.env (site instance mode; site.mk sets it to the site's .env). The server and ocms migrate refuse to start if it is missing, unreadable or does not parse; a ./.env that does not parse is refused too, a missing one ignored. Variables already in the environment win over the file | ./.env | No |
OCMS_SERVER_HOST | Server host address | localhost | No |
OCMS_SERVER_PORT | Server port number | 8080 | No |
OCMS_ENV | Environment mode (development/production) | development | No |
OCMS_LOG_LEVEL | Log level (debug/info//) |
# Set required environment variable
export OCMS_SESSION_SECRET="$(openssl rand -base64 32)"
# Install repository-managed git hooks (run once per clone)
make install-hooks
# Run with asset compilation
make dev
# Or run without rebuilding assets
make run
| Command | Description |
|---|---|
make dev | Build assets and run development server |
make run | Run development server (no asset build) |
make stop | Stop development server on port 8080 |
make restart | Stop and restart development server |
make build | Build binary to bin/ocms |
make build-prod | Build optimized binary (stripped, trimmed) |
make build-linux-amd64 | Cross-compile no-cgo Linux AMD64 binary |
make build-linux-arm64 | Cross-compile no-cgo Linux ARM64 binary |
make build-darwin-arm64 | Cross-compile no-cgo macOS ARM64 binary |
make build-all-platforms | Build no-cgo Linux AMD64/ARM64 and macOS ARM64 binaries |
make test | Run all tests |
make clean | Remove build artifacts |
make clean-db | Remove database files |
make migrate-up | Apply pending migrations (ocms migrate up) |
make migrate-down | Rollback last migration (ocms migrate down) |
make migrate-status | Show migration status (ocms migrate status) |
make migrate-create | Create new migration file |
make assets | Install npm deps and compile SCSS |
make sqlc | Regenerate sqlc code from SQL queries |
make templ | Regenerate templ Go code from .templ files |
make deploy-binary | Deploy binary to remote server (no custom content) |
make commit-prepare | Proxy to Claude slash command /commit-prepare |
On first run with OCMS_DO_SEED=true, the application seeds a default admin user:
Change these credentials immediately after first login. Where a second factor
is required (OCMS_REQUIRE_MFA, on by default in production), every account,
the seeded admin included, sets up an authenticator app at its first sign-in
and is shown ten single-use recovery codes to keep. Seeding never
recreates [email protected] while any administrator account exists, so a
renamed or replaced admin is not brought back by a restart with
OCMS_DO_SEED=true.
With OCMS_DEMO_MODE=true (requires OCMS_DO_SEED=true), additional demo content is seeded including sample pages, categories, tags, media, and menu items. Two demo users are created:
changeme1234 is rotated away too)See docs/demo-deployment.md for Fly.io deployment and demo configuration details.
# Clone the repository
git clone https://github.com/olegiv/ocms-go.git
cd ocms-go
# Start with Docker Compose (generates session secret automatically)
OCMS_ENV=development OCMS_SESSION_SECRET=$(openssl rand -base64 32) OCMS_DO_SEED=true docker compose up -d
# View logs
docker compose logs -f ocms
Access the admin panel at http://localhost:8080/admin with:
The image defaults to OCMS_ENV=production, which refuses OCMS_DO_SEED=true
and fails to start without OCMS_TRUSTED_PROXIES and OCMS_API_ALLOWED_CIDRS,
so this first, seeded run sets OCMS_ENV=development. Change the admin
credentials before starting in production: production startup refuses to run
while the default seeded credentials are active. A production container also
needs, in a .env file beside docker-compose.yml: OCMS_TRUSTED_PROXIES,
OCMS_API_ALLOWED_CIDRS, OCMS_MFA_KEY (or OCMS_REQUIRE_MFA=false),
OCMS_EMBED_PROXY_TOKEN while the Embed module is
active (it is on a fresh install), and the hCaptcha keys or
OCMS_REQUIRE_FORM_CAPTCHA=false. Compose passes the container every variable
the app reads except the listen address, the port and the /app paths, which
it fixes.
The port is published on 127.0.0.1 only. Set OCMS_PUBLISH_ADDRESS=0.0.0.0
when a reverse proxy on another host, or a container that uses the host's
address, has to reach it. The container runs with no-new-privileges and only
the CHOWN, DAC_READ_SEARCH, SETUID and SETGID capabilities its
entrypoint needs to fix volume ownership and drop to the ocms user; the
switch to ocms clears them. The image filesystem is read-only: the app
writes only to its volumes, temporary files go to TMPDIR=/app/data/tmp on
the data volume, and /tmp is a small tmpfs.
| Command | Description |
|---|---|
docker compose up -d | Start oCMS |
docker compose --profile redis up -d | Start with a Redis container |
docker compose down | Stop services |
docker compose logs -f ocms | View logs |
docker compose build --pull ocms && docker compose up -d | Rebuild local image and restart |
| Volume | Container Path | Purpose |
|---|---|---|
ocms_data | /app/data | SQLite database |
ocms_uploads | /app/uploads | Media uploads |
./custom | /app/custom | Custom themes and modules |
Docker builds use the repository source and generate web/static/dist inside the
builder stage. GitHub release archives are for binary installs and are not used
by Docker Compose.
# Build with version info
docker build \
--build-arg VERSION=$(git describe --tags --always) \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
--build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-t ocms:latest .
Create a .env file for production:
OCMS_SESSION_SECRET=your-secure-secret-key-at-least-32-bytes
# Seals two-factor secrets; production requires a second factor for every account
OCMS_MFA_KEY=your-secure-mfa-key-at-least-32-bytes
OCMS_ENV=production
OCMS_DO_SEED=false
OCMS_ACTIVE_THEME=default
OCMS_TRUSTED_PROXIES=127.0.0.1/32,10.0.0.0/8
OCMS_API_ALLOWED_CIDRS=203.0.113.0/24
# Hardened embed proxy baseline (when embed module/provider is enabled)
# Origin matching is exact (scheme + host). Include all real hostnames.
OCMS_EMBED_ALLOWED_ORIGINS=https://example.com,https://www.example.com
OCMS_EMBED_ALLOWED_UPSTREAM_HOSTS=api.dify.ai
OCMS_REQUIRE_EMBED_ALLOWED_ORIGINS=true
OCMS_REQUIRE_EMBED_ALLOWED_UPSTREAM_HOSTS=true
OCMS_EMBED_PROXY_TOKEN=replace-with-embed-proxy-token
OCMS_WEBHOOK_ALLOWED_HOSTS=hooks.example.com,events.example.com
# Required when OCMS_REQUIRE_FORM_CAPTCHA is enabled (enabled by default in production)
OCMS_HCAPTCHA_SITE_KEY=your-site-key
OCMS_HCAPTCHA_SECRET_KEY=your-secret-key
# Optional: Redis connection (the caches stay in memory per process)
OCMS_REDIS_URL=redis://redis:6379/0
Then start with:
docker compose --profile redis up -d
ocms-go/
├── cmd/ocms/ # Application entry point
├── docs/ # Documentation
│ ├── multi-language.md # Multi-language guide
│ ├── webhooks.md # Webhooks configuration
│ ├── import-export.md # Import/export guide
│ └── reverse-proxy.md # Nginx/Apache/NPM setup
├── internal/
│ ├── auth/ # Password hashing utilities
│ ├── cache/ # Caching layer (memory + Redis)
│ ├── config/ # Configuration loading
│ ├── handler/ # HTTP handlers
│ │ └── api/ # REST API handlers
│ ├── i18n/ # Internationalization (admin UI)
│ ├── imaging/ # Image processing (thumbnails, variants)
│ ├── middleware/ # HTTP middleware (auth, API, rate limiting)
│ ├── model/ # Domain models
│ ├── module/ # Module system (registry, hooks)
│ ├── render/ # Template rendering
│ ├── scheduler/ # Cron-based task scheduler
│ ├── seo/ # SEO utilities (sitemap, robots.txt, meta)
│ ├── service/ # Business logic (media, menus, forms)
│ ├── session/ # Session management
│ ├── store/ # Database layer (sqlc generated)
│ │ ├── migrations/ # Goose SQL migrations
│ │ └── queries/ # sqlc query definitions
│ ├── theme/ # Theme loading and management
│ ├── themes/ # Embedded core themes (default, developer)
│ ├── transfer/ # Import/export functionality
│ ├── util/ # Utility functions (slug generation)
│ └── webhook/ # Webhook system
├── modules/ # Custom modules directory
│ └── example/ # Example module implementation
├── custom/ # User content (mostly gitignored — see custom/README.md)
│ ├── themes/ # Custom themes (override or extend core)
│ └── modules/ # Custom modules (see Custom Modules below)
├── web/
│ ├── static/ # Static assets (CSS, JS)
│ │ └── scss/ # SCSS source files
│ └── templates/ # HTML templates
│ ├── admin/ # Admin panel templates
│ ├── api/ # API documentation templates
│ ├── auth/ # Login/logout templates
│ ├── errors/ # Error pages (404, 403, 500)
│ ├── layouts/ # Base layouts
│ └── partials/ # Reusable components
├── uploads/ # Media uploads directory
├── scripts/ # Build scripts
├── Makefile # Development commands
├── site.mk # Shared make targets for site instances (see below)
├── package.json # npm dependencies (htmx, alpine.js)
└── sqlc.yaml # sqlc configuration
The CMS provides a RESTful API for programmatic access to content.
API requests require a Bearer token in the Authorization header:
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/v2/pages
Create API keys in the admin panel under Settings > API Keys.
The REST surface is /api/v2. The OpenAPI 3.1 spec is generated from Go types via huma and served live at /api/v2/openapi.json (or .yaml). Swagger UI is available at /api/v2/docs.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v2/status | API status | Public |
| GET | /api/v2/auth | Inspect the calling API key | Required |
| GET | /api/v2/pages | List pages | Optional |
| GET | /api/v2/pages/{id} | Get page by ID | Optional |
| GET | /api/v2/pages/slug/{slug} | Get page by slug | Optional |
| POST | /api/v2/pages | Create page | Required |
| PUT | /api/v2/pages/{id} | Update page | Required |
| DELETE | /api/v2/pages/{id} | Delete page | Required |
| GET | /api/v2/media | List media | Optional |
| GET | /api/v2/media/{id} | Get media by ID | Optional |
| POST | /api/v2/media | Upload a single file | Required |
| POST | /api/v2/media/batch | Upload multiple files | Required |
| PUT | /api/v2/media/{id} | Update media metadata | Required |
| DELETE | /api/v2/media/{id} | Delete media | Required |
| GET | /api/v2/tags | List tags | Public |
| POST | /api/v2/tags | Create tag | Required |
| PUT | /api/v2/tags/{id} | Update tag | Required |
| DELETE | /api/v2/tags/{id} | Delete tag | Required |
| GET | /api/v2/categories | List categories (tree or flat) |
{
"data": { ... },
"meta": {
"total": 100,
"page": 1,
"per_page": 20
}
}
{
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": { "title": "Title is required" }
}
}
The opt-in MCP Server module lets AI agents read site content over the Model Context Protocol. Enable it in Admin > Modules, then create an API key with the MCP permission (mcp:access) in Admin > API Keys. The admin page at /admin/mcp shows the endpoint, client configuration snippets, the tool catalog, and the drafts and instructions settings.
claude mcp add --transport http ocms https://example.com/api/mcp \
--header "Authorization: Bearer <YOUR_API_KEY>"
| Tool | Purpose |
|---|---|
get_site_info | Site name, URL, languages, and what the key may access |
search_pages | Full-text search with excerpts |
list_pages / get_page | Browse pages; read one by id or slug as HTML or Markdown |
list_media / get_media | Media library items; get_media adds image variants, folder, and translations |
list_tags / get_tag | Tags with usage counts |
list_categories / get_category | Category tree or flat list |
All tools are read-only. See docs/mcp-module.md for authentication, visibility rules, limits, logging, and security notes.
Core themes (default, developer) are embedded in the binary. To create a custom theme, add a directory in custom/themes/:
custom/themes/my-theme/
├── theme.json # Theme configuration
├── templates/
│ ├── layouts/
│ │ └── base.html # Base layout
│ ├── pages/
│ │ ├── home.html # Homepage template
│ │ ├── page.html # Single page template
│ │ └── 404.html # Not found page
│ └── partials/
│ ├── header.html
│ └── footer.html
└── static/
├── css/
└── js/
To override an embedded theme, create a custom theme with the same name:
custom/themes/default/ # Overrides the embedded 'default' theme
Custom themes with the same name as core themes take priority.
{
"name": "My Theme",
"version": "1.0.0",
"author": "Your Name",
"description": "A custom theme",
"settings": [
{
"key": "primary_color",
"label": "Primary Color",
"type": "color",
"default": "#3b82f6"
}
]
}
Create custom modules to extend functionality:
package mymodule
import (
"database/sql"
"embed"
"github.com/olegiv/ocms-go/internal/module"
"github.com/go-chi/chi/v5"
)
//go:embed locales
var localesFS embed.FS
type MyModule struct {
module.BaseModule
}
func New() *MyModule {
return &MyModule{
BaseModule: module.NewBaseModule("mymodule", "1.0.0", "My custom module"),
}
}
func (m *MyModule) RegisterRoutes(r chi.Router) {
r.Get("/my-endpoint", m.handleEndpoint)
}
func (m *MyModule) Migrations() []module.Migration {
return []module.Migration{
{
Version: 1,
Description: "Create my_table",
Up: func(db *sql.DB) error {
_, err := db.Exec("CREATE TABLE my_table (...)")
return err
},
},
}
}
// TranslationsFS returns embedded locale files for i18n support
func (m *MyModule) TranslationsFS() embed.FS {
return localesFS
}
Add self-registration in register.go:
package mymodule
import "github.com/olegiv/ocms-go/internal/module"
func init() {
module.RegisterCustomModule(New())
}
Then add a registration file — one per module, never a shared list:
// custom/modules/imports_mymodule.go
package modules
import _ "github.com/olegiv/ocms-go/custom/modules/mymodule"
Do not add template functions. AllTemplateFuncs() returns funcs from
active modules only, while html/template resolves names at parse time — so
a theme calling one fails to parse on the next restart after the module is
deactivated, and an unparseable theme is silently dropped rather than erroring.
Avoiding that needs a no-op placeholder in internal/render/render.go, which is
a core edit. Expose data over a route instead; custom/modules/bookmarks/ shows
the shape with GET /bookmarks?favorites=1.
See docs/custom-modules.md for the full guide and custom/modules/bookmarks/ for a working example.
Modules can embed their own translation files:
mymodule/
├── module.go
├── handlers.go
└── locales/
├── en/
│ └── messages.json
└── ru/
└── messages.json
Translation keys should be prefixed with the module name (e.g., mymodule.title).
Modules can be enabled/disabled from the admin UI at Admin > Modules. When a module is disabled:
Several sites can share one checkout of oCMS, each a small repository holding
only its own custom/themes/, custom/modules/, .env and deploy wrappers,
with core symlinked here. Their entire build is shared — the site Makefile is
usually one line:
include core/site.mk
That provides dev, run, build, build-prod, the three cross-builds,
test, migrate-*, sync-modules and clean-modules. Run make help in the
site repo for the full list, and override variables before the include.
sync-modules copies the site's Go modules into this tree so the compiler can
see them. Things worth knowing about it:
make clean-modules removes a site's own copies.custom/modules/.owners, so a module you delete
from your site repo is removed here on your next sync, and two sites picking
the same module name is an error rather than a silent overwrite..templ files are generated in the site repo before copying, so make dev
and make build cannot disagree about the generated output..site-build.lock), so two sites
cannot build into it at once.git clean -fdx here, run make sync-modules from each site that owns
modules before building a binary you intend to deploy — otherwise that binary
omits the others' modules and the deploy scripts ship it to every instance.Set OCMS_DB_PATH in each site's .env; it and OCMS_CUSTOM_DIR /
OCMS_UPLOADS_DIR are resolved against the site directory, because dev and
run execute from this checkout. The targets that run, stop or migrate the
site's server (dev, run, stop, restart, migrate-*) stop when the site
.env is missing rather than fall back to data/ocms.db and port 8080.
See docs/custom-modules.md for the full rationale.
Run all tests:
OCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test ./...
Run tests with verbose output:
OCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test -v ./...
Run tests for a specific package:
OCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test -v ./internal/store/...
Check for vulnerabilities:
govulncheck ./...
Project documentation is published at the GitHub Wiki and managed as a git submodule at wiki/.
# Edit a page
vim wiki/Getting-Started.md
# Publish changes
/update-wiki
# Or manually:
cd wiki
git add -A && git commit -m "Update page" && git push
cd ..
git add wiki
git commit -m "Update wiki submodule"
git clone --recurse-submodules https://github.com/olegiv/ocms-go.git
This project uses a shared submodule at .claude/shared containing reusable Claude Code extensions:
To update the shared Claude Code tools to the latest version:
# Using the slash command (recommended)
/update-submodule
# Or manually
git submodule update --remote --merge
If you are using Codex, Claude slash commands are not registered in the
Codex UI and may show No commands. Use shell/make commands directly,
or ask Codex in chat to run them.
You can run slash-command equivalents through Claude CLI:
./scripts/codex/proxy-claude-command.sh /commit-prepare
./scripts/codex/proxy-claude-command.sh /commit-do
./scripts/codex/proxy-claude-command.sh /code-quality
./scripts/codex/proxy-claude-command.sh /security-audit
The proxy runs claude -p with --permission-mode dontAsk and a per-command
--allowedTools list. Rules for tools that run project or package code are
exact (scripts/test-gate.sh, go vet ./..., golangci-lint run ./...,
npm audit); git commands are allowed by prefix, so git commit runs the
repository's hooks and git diff --output=<file> can write a file. Anything
else is denied without a prompt, so a check that needs another tool is skipped
and reported by the command rather than run. This is a best-effort fence, not
a sandbox. Avoid --dangerously-skip-permissions: these
commands read diffs, logs and scan output, and text in them could otherwise
steer the session into any shell command.
Codex wrapper commands (Claude proxy by default):
./scripts/codex-commands code-quality
./scripts/codex-commands security-audit
./scripts/codex-commands commit-prepare
./scripts/codex-commands commit-do
# Explicit local fallback scripts
./scripts/codex-commands code-quality-local
./scripts/codex-commands security-audit-local
./scripts/codex-commands commit-prepare-local
./scripts/codex-commands commit-do-local
After updating, stage and commit the submodule change if you want to keep it:
git add .claude/shared
git commit -m "Update Claude Code shared submodule"
Copyright (C) 2025-2026 Oleg Ivanchenko
GNU General Public License v3.0 - see LICENSE file for details.
warnerrorinfo |
| No |
OCMS_ERROR_LOG_PATH | Path for a separate error log file (5xx/ERROR+ also go to stdout) | - | No |
OCMS_CUSTOM_DIR | Directory for custom themes (custom modules are compiled in) | ./custom | No |
OCMS_ACTIVE_THEME | Active theme (overrides DB/admin setting) | default | No |
OCMS_SITE_URL | Full site URL written into site config on startup (overrides DB/admin setting). Required for RSS/Atom feeds, the sitemap and agent-discovery documents, which answer 503 while it is unset. Must be scheme and host only, e.g. https://example.com — a path, query, fragment, credentials or an out-of-range port fails startup, because routes are served from the root | - | No |
OCMS_DO_SEED | Seed database with default admin and config | false | No |
OCMS_CACHE_TTL | TTL in seconds for the Redis (or fallback) cache, which the caches do not use yet; the in-memory caches have fixed TTLs | 3600 | No |
OCMS_REDIS_URL | Redis URL; the connection shows in the cache admin, but the caches stay in memory per process | - | No |
OCMS_CACHE_PREFIX | Redis key prefix | ocms: | No |
OCMS_CACHE_MAX_SIZE | Max entries for the fallback memory cache, which the caches do not use yet | 10000 | No |
OCMS_HCAPTCHA_SITE_KEY | hCaptcha site key for login protection | - | No |
OCMS_HCAPTCHA_SECRET_KEY | hCaptcha secret key for login protection | - | No |
OCMS_HCAPTCHA_DISABLED | Force-disable hCaptcha regardless of database settings | false | No |
OCMS_GEOIP_DB_PATH | Path to GeoLite2-Country.mmdb for country detection | - | No |
OCMS_UPLOADS_DIR | Directory for uploaded media files | ./uploads | No |
OCMS_TRUSTED_PROXIES | Trusted reverse-proxy CIDRs/IPs; forwarding headers are ignored unless peer is trusted | - | No |
OCMS_REQUIRE_TRUSTED_PROXIES | Fail startup in production if trusted proxy CIDRs/IPs are not configured | false (true in production when unset) | No |
OCMS_API_ALLOWED_CIDRS | Global source CIDRs/IPs allowed to use API keys | - | No |
OCMS_REQUIRE_API_ALLOWED_CIDRS | Fail startup in production, and API key auth elsewhere, when global API source CIDRs are not configured | false (true in production when unset) | No |
OCMS_REQUIRE_API_KEY_EXPIRY | Require API keys to have expiration timestamps | false (true in production when unset) | No |
OCMS_REQUIRE_API_KEY_SOURCE_CIDRS | Require API keys to have per-key source CIDR restrictions | false (true in production when unset) | No |
OCMS_REVOKE_API_KEY_ON_SOURCE_IP_CHANGE | Deactivate API keys when source IP changes and the key has no per-key CIDRs | false (true in production when unset) | No |
OCMS_API_KEY_MAX_TTL_DAYS | Maximum API key lifetime in days (0 disables, max 365) | 0 (90 in production when unset) | No |
OCMS_EMBED_ALLOWED_ORIGINS | Allowed browser origins for public embed proxy routes; required for working browser embed requests in production | - | No |
OCMS_EMBED_ALLOWED_UPSTREAM_HOSTS | Allowed upstream hosts for embed provider API endpoints | - | No |
OCMS_REQUIRE_EMBED_ALLOWED_ORIGINS | Fail startup in production if embed proxy is active without origin allowlist | false (true in production when unset) | No |
OCMS_REQUIRE_EMBED_ALLOWED_UPSTREAM_HOSTS | Fail startup in production if embed proxy is active without upstream host allowlist | false (true in production when unset) | No |
OCMS_EMBED_PROXY_TOKEN | Secret used to mint short-lived signed embed proxy tokens; required for active embed proxy in production | - | No |
OCMS_REQUIRE_EMBED_PROXY_TOKEN | Enforce embed proxy token requirement in non-production too | false | No |
OCMS_REQUIRE_HTTPS_OUTBOUND | Require HTTPS for outbound integration URLs | false (true in production when unset) | No |
OCMS_REQUIRE_FORM_CAPTCHA | Require captcha on all public form submissions | false (true in production when unset) | No |
OCMS_WEBHOOK_FORM_DATA_MODE | form.submitted payload data mode (redacted/none/full) | redacted | No |
OCMS_REQUIRE_WEBHOOK_FORM_DATA_MINIMIZATION | Fail startup in production when form webhook payload mode is full | false (true in production when unset) | No |
OCMS_WEBHOOK_ALLOWED_HOSTS | Allowed destination hosts for active webhook deliveries (exact hostname match) | - | No |
OCMS_REQUIRE_WEBHOOK_ALLOWED_HOSTS | Fail startup in production when active webhooks exist without destination host allowlist | false (true in production when unset) | No |
OCMS_MIGRATOR_ALLOWED_DB_HOSTS | Allowed source-database hosts for the Migrator module (bare hostnames or IPs, no port). Empty = no restriction; migrator routes are admin-only regardless (see docs/migrator-module.md) | - | No |
OCMS_REQUIRE_MIGRATOR_ALLOWED_DB_HOSTS | Fail startup in production when the Migrator module is active without a source-database host allowlist | false (true in production when unset) | No |
OCMS_MIGRATOR_ALLOWED_FILE_ROOTS | Comma-separated absolute roots from which the Migrator may read local media. DRUPAL_FILES, ELEFANT_FILES and PHPNUKE_FILES are also trusted roots; media import fails closed when none are configured | - | No |
OCMS_SANITIZE_PAGE_HTML | Sanitize page HTML before rendering to visitors | false (true in production when unset) | No |
OCMS_REQUIRE_SANITIZE_PAGE_HTML | Fail startup in production if page HTML sanitization is disabled | false (true in production when unset) | No |
OCMS_BLOCK_SUSPICIOUS_PAGE_HTML | Reject page writes containing suspicious HTML patterns | false (true in production when unset) | No |
OCMS_REQUIRE_BLOCK_SUSPICIOUS_PAGE_HTML | Fail startup in production when suspicious page markup blocking is disabled or existing pages contain suspicious markers | false (true in production when unset) | No |
OCMS_HSTS_PRELOAD | Append ; preload to the HSTS header in production; only enable after submitting to the HSTS preload list (see docs/reverse-proxy.md) | false | No |
OCMS_DEMO_MODE | Enable demo content seeding (users, pages, media) | false | No |
make commit-do |
Proxy to Claude slash command /commit-do |
make code-quality | Proxy to Claude slash command /code-quality |
make security-audit | Proxy to Claude slash command /security-audit |
make commit-prepare-local | Run the test gate, then draft a commit message locally |
make commit-do-local | Commit the staged files with the drafted message after the test gate |
make code-quality-local | Run local code quality shell script (golangci-lint, nilaway, go test) |
make security-audit-local | Run local security audit shell script (writes to .audit/) |
make install-hooks | Configure git to use repository-managed hooks from .githooks |
make check-no-absolute-paths | Fail if tracked files contain local absolute paths (/Users/..., /home/..., C:\Users\...) |
| Public |
| POST | /api/v2/categories | Create category | Required |
| PUT | /api/v2/categories/{id} | Update category | Required |
| DELETE | /api/v2/categories/{id} | Delete category | Required |
| GET | /api/v2/openapi.json | OpenAPI 3.1 spec (JSON) | Public |
| GET | /api/v2/openapi.yaml | OpenAPI 3.1 spec (YAML) | Public |
| GET | /api/v2/docs | Swagger UI | Public |
| GET | /health | Health check | Public |