Read the Google client secret from a command
GOOGLE_CLIENT_SECRET_COMMAND runs once at boot and its output is the
secret, so it can come from a file or 1Password instead of the
environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
change
commit 1aef6c2b86f0962d4ed77330a3e2b1dd690d53be
author Claude <noreply@anthropic.com>
date
parent 79c43899
diff --git a/AGENTS.md b/AGENTS.md
index 34d08a7..1358e0a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,8 +85,10 @@ fly.toml           — Fly config for the main demo and PR previews
 
 - `DATABASE_URL` — SQLite path (default: `:memory:`)
 - `SESSION_SECRET` — session signing key (auto-generated if unset)
-- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (required) — a Google OAuth
-  web client with the YouTube Data API enabled, for connecting Google.
+- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET_COMMAND` (required) — a
+  Google OAuth web client with the YouTube Data API enabled, for
+  connecting Google. The secret is whatever the command prints, run once
+  at boot (e.g. `op read op://…` or `cat ~/.config/rank-king/secret`).
   Its redirect URI is `<app URL>/settings/google/callback`. A consent
   screen left in Testing mode expires refresh tokens after 7 days. The
   Fly demos set placeholders in `fly.toml`, since anyone can open them.
diff --git a/Steepfile b/Steepfile
index 4ef2b3c..c928741 100644
--- a/Steepfile
+++ b/Steepfile
@@ -10,5 +10,5 @@ target :lib do
   ignore "lib/rank_king/views", inline: true
   signature "sig"
 
-  library "securerandom", "net-http", "json", "uri"
+  library "securerandom", "open3", "net-http", "json", "uri"
 end
diff --git a/fly.toml b/fly.toml
index 1f9ced0..4cb066b 100644
--- a/fly.toml
+++ b/fly.toml
@@ -22,7 +22,7 @@ primary_region = 'sjc'
 # a demo fails at Google's consent screen.
 [env]
   GOOGLE_CLIENT_ID = 'demo'
-  GOOGLE_CLIENT_SECRET = 'demo'
+  GOOGLE_CLIENT_SECRET_COMMAND = 'echo demo'
 
 [processes]
   app = 'rackup -o 0.0.0.0 demo.ru'
diff --git a/lib/rank_king/config.rb b/lib/rank_king/config.rb
index ffaae54..3a4204f 100644
--- a/lib/rank_king/config.rb
+++ b/lib/rank_king/config.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require "open3"
 require "securerandom"
 
 module RankKing
@@ -22,11 +23,23 @@ module RankKing
       new(
         database_url: env.fetch("DATABASE_URL", ":memory:"),
         session_secret: env.fetch("SESSION_SECRET") { SecureRandom.hex(64) },
-        # A Google OAuth client, for connecting YouTube.
+        # A Google OAuth client, for connecting YouTube. The secret comes
+        # from a command, so it can live in a file or a password manager
+        # rather than the environment.
         google_client_id: env.fetch("GOOGLE_CLIENT_ID"),
-        google_client_secret: env.fetch("GOOGLE_CLIENT_SECRET"),
+        google_client_secret: secret_from(env.fetch("GOOGLE_CLIENT_SECRET_COMMAND")),
       )
     end
+
+    # Runs a shell command, such as `op read op://…` or `cat path`, and
+    # returns what it prints.
+    #: (String command) -> String
+    def self.secret_from(command)
+      output, status = Open3.capture2(command)
+      raise "#{command} exited with #{status.exitstatus}" unless status.success?
+
+      output.strip
+    end
   end
 
   CONFIG = Config.from_env
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2a1783e..baf903d 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -4,4 +4,4 @@ ENV["DATABASE_URL"] = ":memory:"
 # The Google client the app requires. No request reaches Google: the
 # YouTube tests' app subclass hands out a fake one.
 ENV["GOOGLE_CLIENT_ID"] = "client-id"
-ENV["GOOGLE_CLIENT_SECRET"] = "client-secret"
+ENV["GOOGLE_CLIENT_SECRET_COMMAND"] = "echo client-secret"