You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The root pino instance at src/logger.ts:10-13 is configured with only
two options -- level and (in development) transport -- and has NO redact path list. Every logger.error(err, message) site therefore
serialises errors through pino's default err serializer, which walks
enumerable own properties of the Error object. That matters because
the hot error paths in this codebase catch errors that carry secrets
on custom properties:
src/app.ts:82 forwards raw errors from @octokit/webhooks via app.webhooks.onError. When signature verification fails or a
downstream handler throws, the thrown error can retain the webhook
HTTP request, including the X-Hub-Signature-256 header and the
raw body used to build the prompt.
src/orchestrator/connection-handler.ts:537 logs the error returned
by ctx.octokit.auth called with installation type. Octokit
surfaces these as RequestError instances with a request
property: err.request.headers.authorization holds the App JWT
used to mint the installation token, and err.response.data can
echo the JWT back in the GitHub 401 body. With no redaction, both
end up in the structured log.
src/daemon/job-executor.ts:307 logs any throw from runPipeline.
The pipeline at src/core/pipeline.ts:140-142 minted the
installation token, and a second new Octokit is built with that installationToken at src/daemon/job-executor.ts:219. A RequestError from any call on that client carries err.request.headers.authorization with value token ghs_...
verbatim.
src/orchestrator/valkey.ts:128 logs the error on Valkey connect
failure. Bun's RedisClient surfaces the connection URL in err.message on ECONNREFUSED, and the existing redactValkeyUrl
helper at src/orchestrator/valkey.ts:64 is only applied at one
info log site (valkey.ts:33) -- not to the error path.
The codebase already demonstrates awareness of the problem with two
point-solution redactors: redactGitHubTokens at src/utils/sanitize.ts:77-89 (applied only to prompt content, not
logs) and redactValkeyUrl at src/orchestrator/valkey.ts:62-73
(applied only at one call site). Neither runs on log output. The
improvement is to add a single pino redact paths list plus an err
serializer that feeds err.message, err.stack, and err.request
through the already-tested redactGitHubTokens regex -- a ~15-line
change to src/logger.ts with no new dependencies.
Diagram
flowchart LR
A1["octokit webhook error<br/>app.ts:82"]:::src
A2["orchestrator mint<br/>connection-handler.ts:537"]:::src
A3["daemon pipeline<br/>job-executor.ts:307"]:::src
A4["valkey connect<br/>valkey.ts:128"]:::src
ERR["Error instance<br/>err.request.headers.authorization<br/>err.message with secrets"]:::leak
SER["pino.stdSerializers.err<br/>walks enumerable own props"]:::risk
CUR["Current logger<br/>logger.ts 10-13<br/>NO redact configured"]:::risk
OUT["stdout JSON<br/>shipped to log aggregation<br/>retained for weeks"]:::risk
PROP["Existing but unused<br/>redactGitHubTokens<br/>sanitize.ts 77-89"]:::ok
FIX1["Add pino redact paths<br/>authorization, token,<br/>webhookSecret, privateKey"]:::fix
FIX2["Compose err serializer<br/>scrub message and stack"]:::fix
OUT2["stdout JSON with<br/>Redacted placeholders"]:::ok
A1 --> ERR
A2 --> ERR
A3 --> ERR
A4 --> ERR
ERR --> SER --> CUR --> OUT
PROP -. reuse .-> FIX2
FIX1 --> OUT2
FIX2 --> OUT2
classDef src fill:#1f4e79;color:#ffffff;stroke:#0b2545;stroke-width:1px
classDef leak fill:#7a0f12;color:#ffffff;stroke:#4a0000;stroke-width:1px
classDef risk fill:#8a4b00;color:#ffffff;stroke:#5a2f00;stroke-width:1px
classDef ok fill:#196f3d;color:#ffffff;stroke:#0b3a1e;stroke-width:1px
classDef fix fill:#1a5490;color:#ffffff;stroke:#0b2545;stroke-width:1px
Loading
Rationale
The project is a multi-tenant webhook server that handles three
classes of long-lived secrets: GitHub App private keys, the daemon
shared secret (DAEMON_AUTH_TOKEN), and -- for single-tenant OAuth
deployments -- CLAUDE_CODE_OAUTH_TOKEN, a personal Max/Pro
subscription credential that does not auto-rotate. Installation
tokens (the ghs_ prefix) rotate every 60 minutes so their blast
radius is bounded, but the App JWT used to mint them and the OAuth
and daemon tokens are not. docs/OBSERVABILITY.md:3 confirms
structured JSON logs are the primary signal and that log fields are
preserved end-to-end; any long-lived secret that lands in stdout is
then shipped to centralised aggregation and persists for weeks.
Pino's own documentation states that path-based redaction without
wildcards adds roughly 2% overhead to JSON serialization -- effectively
free relative to the cost of a single secret rotation. Because src/utils/sanitize.ts:77-89 already contains a tested redactGitHubTokens regex covering ghp_, gho_, ghs_, ghr_
and fine-grained github_pat_ prefixes, the fix can reuse that
helper inside a composed err serializer rather than duplicating
logic.
The scope of change is small and bounded: a single edit to src/logger.ts plus a matching note in docs/OBSERVABILITY.md. It
does not require a new dependency, a new env var, or any changes to
the ~27 files that call logger.* today.
References
Internal:
src/logger.ts:10-13 -- pino root logger with no redact option
src/app.ts:82 -- webhook error handler that logs the raw err
src/daemon/job-executor.ts:219,307 -- Octokit construction with installationToken and an unredacted error log on any pipeline throw
src/orchestrator/connection-handler.ts:537 -- logs the error for Failed to mint installation token for job with raw err
src/orchestrator/valkey.ts:62-73,128 -- existing redactValkeyUrl
helper not applied on the error-logging path at line 128
Extend src/logger.ts with a redact.paths list covering authorization, *.authorization, headers.authorization, *.headers.authorization, req.headers.authorization, request.headers.authorization, token, installationToken, privateKey, webhookSecret, anthropicApiKey, claudeCodeOauthToken, daemonAuthToken, awsSecretAccessKey, awsSessionToken, awsBearerTokenBedrock, and *.password.
The default censor value Redacted is acceptable.
Compose a custom err serializer that calls pino.stdSerializers.err first, then runs redactGitHubTokens
from src/utils/sanitize.ts over the resulting message and stack strings, catching secrets embedded in free-form text
rather than in named fields.
Add one unit test per serializer path: (a) an Error with request.headers.authorization set, (b) an Error whose message
contains a literal ghs_ token, (c) a plain log call with a privateKey PEM block field. Assert the emitted JSON contains
no secret material.
Update docs/OBSERVABILITY.md with a new Redaction section
listing the redacted paths so operators know what will NOT appear
in logs, and drop a one-line reminder next to redactValkeyUrl
and redactGitHubTokens that the logger is now the canonical
chokepoint.
Optionally, fold redactValkeyUrl into the logger configuration
so the remaining ad-hoc call at src/orchestrator/valkey.ts:33
stops diverging from the global policy.
Areas Evaluated
src/logger.ts -- root pino logger configuration (the focus)
All 27 files under src/ that import logger or call logger.* / ctx.log.*, sampled for error-logging patterns
src/utils/sanitize.ts -- existing redaction helpers for prompts
Finding
The root pino instance at
src/logger.ts:10-13is configured with onlytwo options --
leveland (in development)transport-- and has NOredactpath list. Everylogger.error(err, message)site thereforeserialises errors through pino's default
errserializer, which walksenumerable own properties of the Error object. That matters because
the hot error paths in this codebase catch errors that carry secrets
on custom properties:
src/app.ts:82forwards raw errors from@octokit/webhooksviaapp.webhooks.onError. When signature verification fails or adownstream handler throws, the thrown error can retain the webhook
HTTP request, including the
X-Hub-Signature-256header and theraw body used to build the prompt.
src/orchestrator/connection-handler.ts:537logs the error returnedby
ctx.octokit.authcalled with installation type. Octokitsurfaces these as
RequestErrorinstances with arequestproperty:
err.request.headers.authorizationholds the App JWTused to mint the installation token, and
err.response.datacanecho the JWT back in the GitHub 401 body. With no redaction, both
end up in the structured log.
src/daemon/job-executor.ts:307logs any throw fromrunPipeline.The pipeline at
src/core/pipeline.ts:140-142minted theinstallation token, and a second
new Octokitis built with thatinstallationTokenatsrc/daemon/job-executor.ts:219. ARequestErrorfrom any call on that client carrieserr.request.headers.authorizationwith valuetoken ghs_...verbatim.
src/orchestrator/valkey.ts:128logs the error on Valkey connectfailure. Bun's
RedisClientsurfaces the connection URL inerr.messageonECONNREFUSED, and the existingredactValkeyUrlhelper at
src/orchestrator/valkey.ts:64is only applied at oneinfo log site (
valkey.ts:33) -- not to the error path.The codebase already demonstrates awareness of the problem with two
point-solution redactors:
redactGitHubTokensatsrc/utils/sanitize.ts:77-89(applied only to prompt content, notlogs) and
redactValkeyUrlatsrc/orchestrator/valkey.ts:62-73(applied only at one call site). Neither runs on log output. The
improvement is to add a single pino
redactpaths list plus anerrserializer that feeds
err.message,err.stack, anderr.requestthrough the already-tested
redactGitHubTokensregex -- a ~15-linechange to
src/logger.tswith no new dependencies.Diagram
flowchart LR A1["octokit webhook error<br/>app.ts:82"]:::src A2["orchestrator mint<br/>connection-handler.ts:537"]:::src A3["daemon pipeline<br/>job-executor.ts:307"]:::src A4["valkey connect<br/>valkey.ts:128"]:::src ERR["Error instance<br/>err.request.headers.authorization<br/>err.message with secrets"]:::leak SER["pino.stdSerializers.err<br/>walks enumerable own props"]:::risk CUR["Current logger<br/>logger.ts 10-13<br/>NO redact configured"]:::risk OUT["stdout JSON<br/>shipped to log aggregation<br/>retained for weeks"]:::risk PROP["Existing but unused<br/>redactGitHubTokens<br/>sanitize.ts 77-89"]:::ok FIX1["Add pino redact paths<br/>authorization, token,<br/>webhookSecret, privateKey"]:::fix FIX2["Compose err serializer<br/>scrub message and stack"]:::fix OUT2["stdout JSON with<br/>Redacted placeholders"]:::ok A1 --> ERR A2 --> ERR A3 --> ERR A4 --> ERR ERR --> SER --> CUR --> OUT PROP -. reuse .-> FIX2 FIX1 --> OUT2 FIX2 --> OUT2 classDef src fill:#1f4e79;color:#ffffff;stroke:#0b2545;stroke-width:1px classDef leak fill:#7a0f12;color:#ffffff;stroke:#4a0000;stroke-width:1px classDef risk fill:#8a4b00;color:#ffffff;stroke:#5a2f00;stroke-width:1px classDef ok fill:#196f3d;color:#ffffff;stroke:#0b3a1e;stroke-width:1px classDef fix fill:#1a5490;color:#ffffff;stroke:#0b2545;stroke-width:1pxRationale
The project is a multi-tenant webhook server that handles three
classes of long-lived secrets: GitHub App private keys, the daemon
shared secret (
DAEMON_AUTH_TOKEN), and -- for single-tenant OAuthdeployments --
CLAUDE_CODE_OAUTH_TOKEN, a personal Max/Prosubscription credential that does not auto-rotate. Installation
tokens (the
ghs_prefix) rotate every 60 minutes so their blastradius is bounded, but the App JWT used to mint them and the OAuth
and daemon tokens are not.
docs/OBSERVABILITY.md:3confirmsstructured JSON logs are the primary signal and that log fields are
preserved end-to-end; any long-lived secret that lands in stdout is
then shipped to centralised aggregation and persists for weeks.
Pino's own documentation states that path-based redaction without
wildcards adds roughly 2% overhead to JSON serialization -- effectively
free relative to the cost of a single secret rotation. Because
src/utils/sanitize.ts:77-89already contains a testedredactGitHubTokensregex coveringghp_,gho_,ghs_,ghr_and fine-grained
github_pat_prefixes, the fix can reuse thathelper inside a composed
errserializer rather than duplicatinglogic.
The scope of change is small and bounded: a single edit to
src/logger.tsplus a matching note indocs/OBSERVABILITY.md. Itdoes not require a new dependency, a new env var, or any changes to
the ~27 files that call
logger.*today.References
Internal:
src/logger.ts:10-13-- pino root logger with noredactoptionsrc/app.ts:82-- webhook error handler that logs the rawerrsrc/daemon/job-executor.ts:219,307-- Octokit construction withinstallationTokenand an unredacted error log on any pipeline throwsrc/orchestrator/connection-handler.ts:537-- logs the error forFailed to mint installation token for jobwith rawerrsrc/orchestrator/valkey.ts:62-73,128-- existingredactValkeyUrlhelper not applied on the error-logging path at line 128
src/utils/sanitize.ts:77-89-- testedredactGitHubTokensregexsrc/core/pipeline.ts:140-142-- installation-token retrieval viactx.octokit.authsrc/config.ts:13-15,37-38,55,152,500-501-- secret-bearing configfields:
privateKey,webhookSecret,anthropicApiKey,claudeCodeOauthToken,awsBearerTokenBedrock,daemonAuthTokendocs/OBSERVABILITY.md:3-- no redaction guidance present todayExternal:
errserializer iterates enumerable own properties, soerr.request.headers.authorizationon an Octokit RequestError ends up in structured outputreq.headers.authorizationpatternSuggested Next Steps
src/logger.tswith aredact.pathslist coveringauthorization,*.authorization,headers.authorization,*.headers.authorization,req.headers.authorization,request.headers.authorization,token,installationToken,privateKey,webhookSecret,anthropicApiKey,claudeCodeOauthToken,daemonAuthToken,awsSecretAccessKey,awsSessionToken,awsBearerTokenBedrock, and*.password.The default censor value Redacted is acceptable.
errserializer that callspino.stdSerializers.errfirst, then runsredactGitHubTokensfrom
src/utils/sanitize.tsover the resultingmessageandstackstrings, catching secrets embedded in free-form textrather than in named fields.
request.headers.authorizationset, (b) an Error whosemessagecontains a literal
ghs_token, (c) a plain log call with aprivateKeyPEM block field. Assert the emitted JSON containsno secret material.
docs/OBSERVABILITY.mdwith a new Redaction sectionlisting the redacted paths so operators know what will NOT appear
in logs, and drop a one-line reminder next to
redactValkeyUrland
redactGitHubTokensthat the logger is now the canonicalchokepoint.
redactValkeyUrlinto the logger configurationso the remaining ad-hoc call at
src/orchestrator/valkey.ts:33stops diverging from the global policy.
Areas Evaluated
src/logger.ts-- root pino logger configuration (the focus)src/that importloggeror calllogger.*/ctx.log.*, sampled for error-logging patternssrc/utils/sanitize.ts-- existing redaction helpers for promptssrc/orchestrator/valkey.ts-- existing URL redaction helpersrc/config.ts-- nine secret-bearing config fieldssrc/app.ts-- top-level webhook error handlersrc/daemon/job-executor.ts+src/core/pipeline.ts-- theinstallation-token lifecycle and the errors that can surface it
src/orchestrator/connection-handler.ts-- token mint pathdocs/OBSERVABILITY.md-- confirmed no redaction guidance todaygh issue list --label research --state allreturned no
area: observabilityentriesGenerated by scheduled research workflow run #24850942493 on 2026-04-23