| Date | Learning |
|---|---|
| 2026-04-14 | Constitution forbids context window as a deferral excuse |
| 2026-04-14 | docs/cli/system.md and embed/cmd/system.go diverged on bootstrap promotion intent |
| 2026-04-14 | Raft-lite trade-off is the load-bearing choice in internal/hub |
| 2026-04-14 | AST stutter test only checks FuncDecl, not GenDecl |
| 2026-04-14 | Brand-name handling in title-case engines must cover possessives |
| 2026-04-13 | GPG signing from non-TTY contexts requires pinentry-mac (or equivalent) |
| 2026-04-13 | Load average measures a queue, not CPU utilization |
| 2026-04-13 | rc.ContextDir() is the single source of truth — fix the resolver, not callers |
| 2026-04-09 | Pad index shifting is a real UX bug in batch operations |
| 2026-04-08 | fmt.Fprintf to strings.Builder silently discards errors |
| 2026-04-08 | AST audit tests must cover unexported functions too |
| 2026-04-06 | Agents ignore system-reminder content without explicit relay instructions |
| 2026-04-04 | Format-verb strings are localizable text, not exempt from magic string checks |
| 2026-04-04 | Agents add allowlist entries to make tests pass — guard every exemption |
| 2026-04-03 | Subagent scope creep and cleanup (consolidated) |
| 2026-04-03 | Bulk rename and replace_all hazards (consolidated) |
| 2026-04-03 | Import cycles and package splits (consolidated) |
| 2026-04-03 | Lint suppression and gosec patterns (consolidated) |
| 2026-04-03 | Skill lifecycle and promotion (consolidated) |
| 2026-04-03 | Cross-cutting change ripple (consolidated) |
| 2026-04-03 | Dead code detection (consolidated) |
| 2026-04-03 | desc.Text() is the single highest-connectivity symbol in the codebase |
| 2026-04-01 | Raw I/O migration unlocks downstream checks for free |
| 2026-04-01 | go/packages respects build tags — darwin-only violations invisible on Linux |
| 2026-04-01 | Copilot CLI skills need a sync mechanism to prevent drift from ctx skills |
| 2026-04-01 | Contributor PRs based on older code reintroduce removed features |
| 2026-03-31 | Magic string cleanup compounds: each pass reveals the next layer |
| 2026-03-31 | Force-loaded behavioral prose gets ignored — action-gating hooks don't |
| 2026-03-31 | Legacy key directory cleanup was specified but not automated |
| 2026-03-31 | Convention audits must check cmd/ purity, not just types and docstrings |
| 2026-03-31 | JSON Schema default fields cause linter errors with some validators |
| 2026-03-30 | Architecture diagrams drift silently during feature additions |
| 2026-03-30 | Python-generated doc.go files need gofmt — formatter strips bare // padding lines |
| 2026-03-30 | lint-docstrings.sh greedy sed hid all return-type violations |
| 2026-03-25 | Machine-generated CLAUDE.md content consumes per-turn budget without proportional value |
| 2026-03-25 | Template improvements don't propagate to existing projects |
| 2026-03-24 | lint-drift false positives from conflating constant namespaces |
| 2026-03-24 | git describe --tags follows ancestry, not global tag list |
| 2026-03-23 | Typography detection script needs exclusion lists for intentional uses |
| 2026-03-23 | Splitting core/ into subpackages reveals hidden structure |
| 2026-03-23 | Higher-order callbacks in param structs are a code smell |
| 2026-03-20 | Commit messages containing script paths trigger PreToolUse hooks |
| 2026-03-18 | Lazy sync.Once per-accessor is a code smell for static embedded data |
| 2026-03-17 | Write package output census: 69 trivial/simple, 38 consolidation candidates, 18 complex |
| 2026-03-16 | Docstring tasks require reading CONVENTIONS.md Documentation section first |
| 2026-03-16 | Convention enforcement needs mechanical verification, not behavioral repetition |
| 2026-03-16 | One-liner method wrappers hide dependencies without adding value |
| 2026-03-16 | Agents reliably introduce gofmt issues during bulk renames |
| 2026-03-15 | Contributor PRs need post-merge follow-up commits for convention alignment |
| 2026-03-15 | Grep for callers must cover entire working tree before deleting functions |
| 2026-03-14 | Stderr error messages are user-facing text that belongs in assets |
| 2026-03-14 | Hardcoded _alt suffixes create implicit language favoritism |
| 2026-03-13 | sync-why mechanism existed but was not wired to build |
| 2026-03-12 | Project-root files vs context files are distinct categories |
| 2026-03-12 | Constants belong in their domain package not in god objects |
| 2026-03-07 | Always search for existing constants before adding new ones |
| 2026-03-07 | SafeReadFile requires split base+filename paths |
| 2026-03-06 | Stale directory inodes cause invisible files over SSH |
| 2026-03-06 | Stats sort uses string comparison on RFC3339 timestamps with mixed timezones |
| 2026-03-06 | Claude Code supports PreCompact and SessionStart hooks that ctx does not use |
| 2026-03-06 | Package-local err.go files invite broken windows from future agents |
| 2026-03-05 | State directory accumulates silently without auto-prune |
| 2026-03-05 | Global tombstones suppress hooks across all sessions |
| 2026-03-05 | Claude Code has two separate memory systems behind feature flags |
| 2026-03-05 | Blog post editorial feedback is higher-leverage than drafting |
| 2026-03-04 | CONSTITUTION hook compliance is non-negotiable — don't work around it |
| 2026-03-02 | Hook message registry test enforces exhaustive coverage of embedded templates |
| 2026-03-02 | Existing Projects is ambiguous framing for migration notes |
| 2026-03-02 | Claude Code JSONL model ID does not distinguish 200k from 1M context |
| 2026-03-01 | Gosec G306 flags test file WriteFile with 0644 permissions |
| 2026-03-01 | Converting PersistentPreRun to PersistentPreRunE changes exit behavior |
| 2026-03-01 | Test HOME isolation is required for user-level path functions |
| 2026-03-01 | Task descriptions can be stale in reverse — implementation done but task not marked complete |
| 2026-03-01 | Model-to-window mapping requires ordered prefix matching |
| 2026-03-01 | TASKS.md template checkbox syntax inside HTML comments is parsed by RegExTaskMultiline |
| 2026-03-01 | Hook logs had no rotation; event log already did |
| 2026-02-28 | ctx pad import, ctx pad export, and ctx system resources make three hack scripts redundant |
| 2026-02-28 | Getting-started docs assumed Claude Code as the only agent |
| 2026-02-28 | Plugin reload script must rebuild cache, not just delete it |
| 2026-02-27 | site/ directory must be committed with docs changes |
| 2026-02-27 | Doctor token_budget vs context_window confusion |
| 2026-02-27 | Drift detector false positives on illustrative code examples |
| 2026-02-27 | Context injection and compliance strategy (consolidated) |
| 2026-02-26 | Webhook silence after ctxrc profile swap is the most common notify debugging red herring |
| 2026-02-26 | Documentation drift and auditing (consolidated) |
| 2026-02-26 | Agent context loading and task routing (consolidated) |
| 2026-02-26 | Go testing patterns (consolidated) |
| 2026-02-26 | PATH and binary handling (consolidated) |
| 2026-02-26 | Task management and exit criteria (consolidated) |
| 2026-02-26 | Agent behavioral patterns (consolidated) |
| 2026-02-26 | Hook compliance and output routing (consolidated) |
| 2026-02-26 | ctx add and decision recording (consolidated) |
| 2026-02-24 | CLI tools don't benefit from in-memory caching of context files |
| 2026-02-22 | Hook behavior and patterns (consolidated) |
| 2026-02-22 | UserPromptSubmit hook output channels (consolidated) |
| 2026-02-22 | Linting and static analysis (consolidated) |
| 2026-02-22 | Permission and settings drift (consolidated) |
| 2026-02-22 | Gitignore and filesystem hygiene (consolidated) |
| 2026-01-28 | IDE is already the UI |
Context: Mid-session, agent proposed pacing through doc.go rewrites with the reasoning that context budget was tight.
Lesson: The CONSTITUTION explicitly lists 'We are running out of context window' as a forbidden deferral phrase under No Excuse Generation. The rule is real and applies to agent self-pacing, not just user-facing answers.
Application: When tempted to scope down because context is tight, re-read the constitution. The right move is to do the work end-to-end, not to ask the user which slice to skip.
[2026-04-14-010134] docs/cli/system.md and embed/cmd/system.go diverged on bootstrap promotion intent
Context: Header comment in internal/config/embed/cmd/system.go claimed bootstrap was promoted to top-level; the bootstrap.go registration never actually promoted it. Two contradictory sources of truth coexisted silently.
Lesson: Header-comment claims about command-tree structure are unaudited; they can drift from registrations without any test failing. Trust the code, not the comment.
Application: When evaluating any package_name namespace cleanup type claim about command structure, verify against the actual cobra registration in internal/bootstrap/group.go before acting.
Context: Discovered while writing thorough doc.go for internal/hub. The package embeds HashiCorp Raft for leader election only; data replication is sequence-based gRPC sync over the append-only JSONL store.
Lesson: A leader crash window between accept and replicate can lose the most recent write. Append-only storage plus idempotent clients make this acceptable; full Raft log replication would not be needed and would not be simpler.
Application: Any future make hub stronger proposal must engage with this trade-off explicitly. Do not abandon Raft-lite accidentally by introducing log-replicated state; that would invalidate the simplicity argument.
Context: tpl.TplEntryMarkdown stuttered for a long time because TestNoStutteryFunctions in internal/audit walks *ast.FuncDecl only; the constant slipped through.
Lesson: The audit suite has a real coverage gap for *ast.GenDecl (consts, vars, types). Stuttery type/const names will not be caught until the audit is extended to walk those node kinds.
Application: When a stuttery identifier is reported by a human, check both the offending file and whether the audit can catch it; if not, file an audit-extension task.
Context: First pass of hack/title-case-headings.py produced 'Ctx's' from 'ctx's' because the brand check matched the bare token only.
Lesson: A brand allowlist needs to recognize , 's, s, and short apostrophe-suffixed variants. Single-word matching misses contractions and possessives.
Application: When adding a new always-lowercase brand to hack/title-case-headings.py, extend the suffix-aware loop in title_case_word, not just the BRAND_LOWER set.
Context: git commit failed from Claude Code's shell with 'gpg: signing failed: No such file or directory' — the default pinentry-curses cannot open a TTY in agent-invoked shells. Manual commits from a real terminal worked fine.
Lesson: GPG's default curses pinentry requires an interactive TTY. In non-TTY contexts (Claude Code, CI, scripts, cron), signing fails silently-ish. The fix is to configure a GUI pinentry that uses the OS keychain: brew install pinentry-mac; echo 'pinentry-program $(brew --prefix)/bin/pinentry-mac' >> ~/.gnupg/gpg-agent.conf; gpgconf --kill gpg-agent. Once the passphrase is saved in Keychain, signing works from any context.
Application: If agents or CI need to sign commits, configure pinentry-mac (macOS) or pinentry-gtk/pinentry-qt (Linux) with the OS keychain, not pinentry-curses. This is a one-time setup per machine.
Context: The 'Load Xx CPU count' resource alert fired at 1.74x while htop showed per-core utilization well under 50% and idle cores. Load average counts runnable + uninterruptible-sleep processes, smoothed over 1/5/15 minutes.
Lesson: Load average and CPU% measure different things. High load with low CPU% typically means many short-lived processes or I/O-bound work (e.g., go test spawning hundreds of parallel test binaries). The 1-minute average is too reactive for dev machines that periodically run test suites — 5-minute smooths transient spikes without hiding sustained pressure.
Application: For alerting thresholds based on system load, prefer 5-minute over 1-minute averages. 1-minute is useful for interactive debugging; 5-minute is better for automated alerts that should not fire on normal build/test activity.
Context: When ctx init failed with a boundary error, my first instinct was to have init bypass rc.ContextDir() and use filepath.Join(cwd, dir.Context) directly. Volkan shut that down: rc.ContextDir() encodes invariants (team shares, symlinks, network mounts, .ctxrc overrides) that individual commands cannot reason about.
Lesson: Resolution chains with multiple fallbacks are contracts. If one command bypasses the chain, it silently diverges from every other command's notion of 'the context directory.' When a resolver produces a wrong answer for a specific case, fix the resolver — don't let callers opt out.
Application: Any time you see rc.ContextDir(), rc.RC(), or similar central resolvers producing a bad result, the fix belongs in the resolver itself (or in its input data like .ctxrc). Caller-side bypasses create drift.
Context: ctx pad rm 10; rm 11; rm 12 deleted wrong entries because indices shifted after each deletion
Lesson: Any ID-based system where users chain operations needs stable IDs. Look-then-act is safe for single ops; look-then-batch-act breaks with shifting indices
Application: Both pad and remind now use stable IDs with batch delete and range support. Apply same pattern to any future numbered-list subsystem
Context: golangci-lint errcheck allows fmt.Fprintf to strings.Builder because Write never fails, but project convention says zero silent discard
Lesson: Linter coverage gaps exist where language guarantees mask conventions. AST tests fill the gap
Application: Created TestNoUncheckedFmtWrite to enforce fmt.Fprintf error handling. Use if _, err := fmt.Fprintf(...) with log.Warn on the error path
Context: TestDocCommentStructure only checked exported functions, so agent-written helpers in format.go had no godoc enforcement
Lesson: Convention enforcement tests must default to scanning all documented functions. Use explicit opt-outs (test files) not opt-ins (exported only)
Application: When adding AST audit tests, scan all functions. We fixed TestDocCommentStructure to drop the IsExported gate and fixed 84 violations
Context: Provenance line (Session: abc | Branch: main @ hash) was emitted by hook but agents in other projects silently ignored it. The line appeared in the system-reminder but the agent treated it as internal metadata.
Lesson: Claude Code surfaces hook stdout as system-reminder tags. Agents only relay content that has explicit display instructions. IMPORTANT: means pay attention internally. Display this line verbatim means show to user. Without the instruction, even correct output is invisible to the user.
Application: Any hook output intended for the user must include an explicit relay instruction like Display this line verbatim at the start of your response. Do not rely on IMPORTANT: alone — it signals internal priority, not user-facing output.
Context: Strings like '%d entries checked' were passing TestNoMagicStrings because the format-verb exemption was too broad
Lesson: Any string containing English words alongside format directives is user-facing text that belongs in YAML assets
Application: Removed format-verb, URL-scheme, HTML-entity, and err/ exemptions from TestNoMagicStrings
Context: Found that every exemption map/allowlist in audit tests is a tempting shortcut for agents
Lesson: Added DO NOT widen guard comments to all 10 exemption data structures across 7 test files
Application: Every new audit test with an exemption must include the guard comment. Review PRs for drive-by allowlist additions.
Consolidated from: 4 entries (2026-03-06 to 2026-03-23)
- Subagents reliably rename functions, restructure files, change import aliases, and modify function signatures beyond their stated scope — even narrowly scoped tasks like fixing em-dashes in comments
- Subagents create new files during refactors but consistently fail to delete the originals — always audit for stale files, duplicate definitions, and orphaned imports afterward
- After any agent-driven refactor: run
git diff --statandgit diff --name-only HEAD, revert anything outside the intended scope, and check for stale package declarations before building
Consolidated from: 3 entries (2026-03-15 to 2026-03-20)
replace_allon short tokens (e.g.core., function names) matches inside longer identifiers and function definitions —remindcore.becomesremindtidy.,func HumanAgobecomesfunc format.DurationAgo(invalid Go)sedinsert-before-first-match does not understand Go import aliases — the alias attaches to whatever line sed inserts, not the original target- For function renames: delete the old definition separately rather than using replace_all. For bulk import additions: check for aliased imports first and handle them separately, or use goimports
Consolidated from: 5 entries (2026-03-06 to 2026-03-22)
- Types in god-object files (e.g. hook/types.go with 15+ types from 8 domains) create circular dependencies — move types to their owning domain package
- Tests in parent package X cannot import X/sub packages that import X back — move tests to the sub-package they exercise
- Variable shadowing causes cascading failures after splits:
dir,file,entryare common Go variable names that collide with new sub-package names — rungo test ./...before committing splits - When moving constants between packages, change imports and all references in a single atomic write so the linter never sees an inconsistent state
- Import cycle rule: the package providing implementation logic must own the
shared types; the facade package aliases them (e.g.
entry.Paramsaliasesadd/core.EntryParams)
Consolidated from: 4 entries (2026-03-04 to 2026-03-19)
- Rename constants to avoid gosec G101 false positives (Tokens->Usage, Passed->OK) instead of adding nolint/nosec/path exclusions — exclusions break on file reorganization
nolint:goconstfor trivial values normalizes magic strings — use config constants instead of suppressing the linternolint:errcheckin tests teaches agents to spread the pattern to production code — uset.Fatal(err)for setup,defer func() { _ = f.Close() }()for cleanup- golangci-lint v2 ignores inline nolint directives for some linters — use
config-level
exclusions.rulesfor gosec patterns, fix the code instead of suppressing errcheck
Consolidated from: 4 entries (2026-03-01 to 2026-03-14)
- Internal skill renames and promotions require synchronized updates across 6+ layers: SKILL.md frontmatter, internal cross-references, external docs, embed_test.go expected list, recipe/reference docs, and plugin cache rebuild + session restart
- Skill behavior changes ripple through hook messages, fallback strings in Go code, doc descriptions, and Makefile hints — grep for the skill name across the entire repo
- Skills without a trigger mechanism (no user invocation, no hook loading) are dead code — audit skills for reachability
- After promoting skills: grep -r for the old name across the whole tree, run plugin-reload.sh, restart session to verify autocomplete, and clean stale Skill() entries from settings.local.json
Consolidated from: 4 entries (2026-02-19 to 2026-03-01)
- Path changes (e.g. key file location) ripple across 15+ doc files and 2 skills — grep broadly (not just code) and budget for 15+ file touches
- Removing embedded asset directories requires synchronized cleanup across 5+ layers: embed directive, accessor functions, callers, tests, config constants, build targets, documentation — work outward from the embed
- Absorbing shell scripts into Go commands creates a discoverability gap — update contributing.md, common-workflows.md, and CLI index as part of the absorption checklist
- A feature without docs is invisible to users: always check feature page, cli-reference.md, relevant recipes, and zensical.toml nav after implementing a new CLI subcommand
Consolidated from: 3 entries (2026-03-15 to 2026-03-30)
- Dead packages can build and test green while being completely unreachable — detection requires checking bootstrap registration, not just build success (e.g. internal/cli/recall/ existed with tests but was never wired into the command tree)
- Files created by
ctx initthat no agent, hook, or skill ever reads are dead on arrival — verify there is at least one consumer before adding to init scaffolding - When touching legacy compat code, first ask whether the legacy path has real users — if not, delete it entirely rather than improving it (MigrateKeyFile had 5 callers and test coverage but zero users)
Context: GitNexus enrichment during architecture analysis revealed desc.Text() (internal/assets/read/desc/desc.go:75) has 30+ direct callers spanning every architectural layer (MCP handler, format, index, tidy, trace, memory, sysinfo, io) and participates in 53 execution flows.
Lesson: TestDescKeyYAMLLinkage is the most critical guard in the codebase — it protects the symbol with the widest blast radius. If YAML text loading breaks, the entire CLI and MCP server output blank strings silently (no crash, no warning).
Application: Treat desc.Text() as a frozen API — add new functions rather than modifying the existing signature. Any change to config/embed/text or assets/read/desc should be followed by running the linkage audit. Monitor this symbol during major refactors.
Context: TestNoRawPermissions had zero violations because the raw I/O migration moved all octal literals into internal/io/ which already used config/fs constants
Lesson: Chokepoint migrations have cascading benefits — centralizing one concern (file I/O) automatically resolves other drift (raw permissions)
Application: Prioritize chokepoint migrations (io, exec, write, err) before smaller checks that depend on them
Context: TestNoExecOutsideExecPkg could not detect violations in _darwin.go files when running on Linux
Lesson: AST checks using go/packages only see files matching the current GOOS. Cross-platform violations need either multi-GOOS CI or a go/parser fallback
Application: When writing audit checks for code with build tags, fix the violations regardless (code correctness) but note that test coverage is platform-dependent
Context: 5 Copilot CLI skills were condensed versions of ctx skills, independently maintained with no drift detection
Lesson: Any time the same content exists in two locations without a sync mechanism, it will drift silently
Application: make sync-copilot-skills added to build deps, make check-copilot-skills added to audit target
Context: PR #45 brought back prompt templates, PROMPT.md, and IMPLEMENTATION_PLAN.md that were explicitly removed in March
Lesson: When resolving contributor merge conflicts, check decisions history for intentional removals — do not assume the PR content is additive
Application: Cross-reference DECISIONS.md before accepting PR content that adds files or features
Context: What started as fix 4 fmt.Fprintf(os.Stderr) calls expanded to over-tokenized format strings, magic hex perms, unstandardized TOML parsing tokens, missing docstrings on new constants — each fix exposed adjacent violations
Lesson: Mechanical cleanup is fractal. The first sweep finds the obvious violations, but fixing them puts adjacent code under scrutiny. Budget for 2-3x the initial estimate
Application: When scoping cleanup tasks, do not commit to done in one pass. Commit after each layer and let the user decide when to stop
Context: AGENT_PLAYBOOK was force-injected at ~14k tokens every session. Agent routinely skipped its Context Readback directive when the user's first message was a concrete task. Meanwhile, hooks that gate actions (qa-reminder, specs-nudge, block-dangerous-commands) were consistently followed because they fire at the moment of violation.
Lesson: Prose instructions compete with the user's immediate request and lose. Hooks that intercept actions at execution time are enforceable. More injected content means less attention per token — slim injection to only what must be internalized before any action.
Application: When adding agent directives, prefer action-gating hooks over injected prose. If it must be injected, keep it small and directive-only. Reserve force-injection for hard rules (CONSTITUTION) and distilled actionable checklists (gate file).
Context: ~/.local/ctx/keys/ accumulated 584 orphan keys from test runs before the v0.8.0 migration to ~/.ctx/.ctx.key
Lesson: Migration specs that call for manual cleanup of old paths should include an automated step — either in the migration code itself or as a post-release cleanup task. Tests that write to global paths must isolate HOME.
Application: When writing migration specs, always include automated cleanup of the old path. When writing tests that touch user-level directories, verify HOME is isolated via t.Setenv.
Context: Placed needsSpec helper in cmd/root/run.go instead of core/entry/predicate.go. Missed it because the audit checklist only covered types and docstrings
Lesson: cmd/ directories must contain only Cmd() and Run*() — all helper functions, unexported logic, and types belong in core/. Added TestCmdDirPurity compliance test to enforce this mechanically
Application: The compliance test now catches this automatically. 28 pre-existing violations grandfathered in the allowlist
Context: ctxrc.schema.json had default: values on 16 fields that triggered incompatible type errors in the user's linter
Lesson: Move default values into the description string instead of using the default keyword — Go rc.*() accessors handle the actual defaults
Application: When adding new .ctxrc fields, document defaults in the description, never use default: in the schema
Context: During the journal-recall merge, architecture-dia-build.md listed 23 CLI packages but 31 existed. 8 packages added over months without updating the diagram.
Lesson: Exhaustive lists and counts in architecture docs go stale every time a package is added. The drift is invisible because nobody re-counts.
Application: After adding a new CLI package, grep architecture diagrams for package counts and directory listings. Consider adding a drift-check comment that validates the count programmatically.
[2026-03-30-003734] Python-generated doc.go files need gofmt — formatter strips bare // padding lines
Context: Batch-generated doc.go files used blank // lines for padding, which gofmt removes as unnecessary whitespace
Lesson: Programmatic Go file generation must produce substantive content lines, not blank comment padding — gofmt enforces this
Application: Always run gofmt after any scripted Go file generation
Context: sed 's/.*) //' consumed return type parens, leaving { — functions with return types were invisible to the script for months
Lesson: Greedy regex in shell scripts can silently suppress entire categories of lint violations — test with edge cases, not just happy paths
Application: When writing sed-based lint checks, test with multi-paren signatures (func Foo() (string, error))
[2026-03-25-234039] Machine-generated CLAUDE.md content consumes per-turn budget without proportional value
Context: GitNexus injected 121 lines (61% of CLAUDE.md) with auto-generated skill pointers like 'Work in the Watch area (39 symbols)' — generic index data loaded on every conversation turn
Lesson: CLAUDE.md is prime real estate — every token competes with project-specific instructions. Auto-generated content belongs in on-demand skills, not in always-loaded files
Application: Audit CLAUDE.md periodically for content that could be delivered via skills instead. Prefer a one-line pointer over inline content for companion tools
Context: 5 of 8 context files in the ctx project itself had stale/missing comment headers — templates evolved but non-destructive init never re-synced them
Lesson: Any template change is invisible to existing users until they run ctx init --force
Application: Added drift detection (checkTemplateHeaders) to ctx drift. Consider surfacing this during ctx status too.
Context: lint-drift.sh checked all string constants in embed/cmd/.go against commands.yaml, but Use constants are cobra syntax strings, not YAML lookup keys
Lesson: Shell grep on constant values cannot distinguish constant types; only DescKey* constants are YAML keys. AST-based analysis is needed for type-aware checks
Application: Already captured in specs/ast-audit-tests.md; the lint-drift fix is shipped in v0.8.0
Context: Release notes skill diffed against v0.3.0 instead of v0.6.0 because the release branch diverged before v0.6.0 was tagged
Lesson: git describe --tags --abbrev=0 follows reachability from HEAD; use git tag --sort=-v:refname | head -1 for the latest tag globally
Application: Any script or skill that needs the latest release should use sorted tag list, not describe
Context: detect-ai-typography.sh flagged config/token/delim.go (intentional delimiter constants) and test files (test data containing em-dashes)
Lesson: Detection scripts for convention enforcement need exclusion patterns for files where the flagged patterns are intentional data, not prose
Application: Add exclusion patterns proactively when creating detection scripts; *_test.go and constant-definition files are common false positive sources
[2026-03-23-003544] Splitting core/ into subpackages reveals hidden structure
Context: init core/ was a flat bag of domain objects — splitting into backup/, claude/, entry/, merge/, plan/, plugin/, project/, prompt/, tpl/, validate/ exposed duplicated logic, misplaced types, and function-pointer smuggling that were invisible in the flat layout
Lesson: Flat core/ packages hide coupling — circular dependency resolution during splits naturally groups related items, increases cohesion, and surfaces objects that don't belong
Application: When a core/ package grows, split it into subpackages even if it creates temporary circular deps — resolving those deps is the design work that reveals the right structure
Context: MergeParams.UpdateFn and DeployParams.ListErr/ReadErr were function pointers where all callers passed thin wrappers varying only by a text key
Lesson: If all callers pass thin wrappers around the same pattern (fmt.Errorf with different keys), the callback is just data in disguise
Application: When a struct field is a function pointer, check if all callers vary only by a string key — if so, replace the callback with the key and let the consumer do the dispatch
Context: Git commit message body contained a path to a shell script under the hack directory which matched a hook pattern that blocks direct script invocation
Lesson: Hooks scan all Bash tool input including heredoc content used for commit messages, not just the command itself
Application: Rephrase commit messages and ctx add content to avoid paths that match hook deny patterns, use generic references instead of literal file paths
Context: assets package had 4 sync.Once guards, 4 exported maps, 4 Load*() functions, and a wrapper desc package — all to lazily load YAML from embed.FS that never mutates. Every accessor call went through sync.Once + global map + wrapper indirection.
Lesson: When data is static and loaded from embedded bytes, scatter-loading with per-accessor sync.Once is over-engineering. A single Init() called eagerly at startup is simpler, and one sync.Once on Init() itself provides the test safety net. Exported maps that exist only for wrapper packages to reach are a sign the abstraction boundary is wrong.
Application: Prefer eager Init() in main.go for static embedded data. Keep maps unexported. Accessors do plain map lookups. If a wrapper package exists solely to break a cycle caused by exported state, delete the wrapper and unexport the state.
[2026-03-17-105637] Write package output census: 69 trivial/simple, 38 consolidation candidates, 18 complex
Context: Full audit of internal/write/ (26 files, 160 functions, 337 Println calls) to evaluate whether block template consolidation is worth a systematic refactor.
Lesson: Only 30% of write functions benefit from output consolidation. The sweet spot is multi-line (16) and conditional (22) functions.
Application: Check function category before consolidating. Trivial/simple stay as-is. Conditional functions need pre-computation before block templates. Loop-based complex functions stay imperative. Don't bulk-refactor.
Context: Agent was asked to review docstrings in server.go but skipped convention loading, missed incomplete Parameter/Returns sections, and needed three hints to recall the known issue
Lesson: Any task involving docstrings, comments, or documentation formatting is a convention-sensitive task — read CONVENTIONS.md (Documentation section) and LEARNINGS.md (for known gaps) before reviewing or writing
Application: On any docstring/comment task: (1) load CONVENTIONS.md Documentation section, (2) check LEARNINGS.md for related entries, (3) audit all functions in scope against the convention template, not just the ones in the diff
Context: Godoc Parameters/Returns sections were missed repeatedly across sessions despite memory entries and feedback
Lesson: System-level brevity instructions outcompete context-injected conventions. Memory shifts probability (~40% to ~70%) but doesn't create invariants. The competing pressures are architectural, not a recall problem.
Application: Invest in linter rules or PreToolUse gates for mechanically-checkable conventions. Reserve behavioral nudges for judgment calls that can't be linted. See ideas/spec-convention-enforcement.md for the three-tier strategy.
Context: checkBoundary() and loadContext() were methods on Handler that just called validation.ValidateBoundary and context.Load with h.ContextDir
Lesson: If a method only passes a struct field to a stdlib function, inline it — the wrapper obscures the real dependency
Application: Before extracting a helper method, check if it just forwards a field to another function. If so, call the function directly.
Context: Subagents renamed consequences->consequence across 75+ files but left formatting errors in 12 Go files
Lesson: Always run gofmt -l after agent-driven refactors before trusting the build
Application: Add gofmt -w pass as a standard step after any agent-driven bulk edit
Context: PR #42 (MCP v0.2) addressed bulk of review feedback but left ~12 inline strings, no embed_test coverage, and substring matching in containsOverlap
Lesson: Merging with known gaps is fine when the gaps are mechanical, but the follow-up must be immediate — track in ideas/done/ with a review status doc
Application: For future contributor PRs: create ideas/pr{N}-review-status.md during review, merge when architecture is sound, fix convention gaps in a same-day follow-up commit
Context: Deleted 7 err/prompt functions as dead code, but callers existed in unstaged refactoring files — caused build failures
Lesson: When the working tree has unstaged changes from a prior session, grep hits only committed+staged code; must grep the full tree or build-test before declaring functions dead
Application: Always run make build after deleting functions, even if grep shows zero callers
Context: Added fmt.Fprintf(os.Stderr) error reporting to event log, initially with inline strings
Lesson: Any string that reaches the user, including stderr warnings, routes through assets.TextDesc() for i18n readiness
Application: When adding stderr output, create text.yaml entries and asset keys first
Context: Session parser had session_prefix_alt hardcoding Turkish as a special case alongside English default
Lesson: Naming a constant _alt and hardcoding one non-English language as a built-in default discriminates by giving that language special status. The pattern doesn't scale (alt_2? alt_3?) and signals that adding languages requires code changes.
Application: When a feature needs multi-value support, use configurable lists from the start — not hardcoded pairs with _alt suffixes. Default to a single canonical value; all extensions are user-configured equally.
Context: assets/why/ had drifted from docs/ — the sync targets existed in the Makefile but build did not depend on sync-why
Lesson: Freshness checks that are not in the critical path will be forgotten. Wire them as build prerequisites, not optional audit steps
Application: Any derived or copied asset should be a prerequisite of build, not just audit
Context: Tried moving ImplementationPlan constant to config/ctx assuming it was a context file. (Note: IMPLEMENTATION_PLAN.md was removed in 2026-03-25 as a dead file — no agent consumer.)
Lesson: Files created by ctx init in the project root (Makefile) are scaffolding, not context files loaded via ReadOrder. They belong in config/file, not config/ctx
Application: Before moving a file constant, check whether it is in ReadOrder (context) or created by init (project-root)
Context: file.go held agent scoring constants, budget percentages, cooldown durations — none related to file config
Lesson: When a constant is only used by one domain (e.g. agent scoring), it should live in that domain's config package
Application: Check callers before placing constants; if all callers are in one domain, the constant belongs there
Context: Added ExtJsonl constant to config/file.go but ExtJSONL already existed with the same value, causing a duplicate
Lesson: Grep for the value (e.g. '.jsonl') across config/ before creating a new constant — naming variations (camelCase vs ALLCAPS) make duplicates easy to miss
Application: Before adding any new constant to internal/config, search by value not just by name
Context: During system/core cleanup, persistence.go passed a full path to validation.SafeReadFile which expects (baseDir, filename) separately
Lesson: Use filepath.Dir(path) and filepath.Base(path) to split full paths when adapting os.ReadFile calls to SafeReadFile
Application: When converting os.ReadFile to SafeReadFile, always check whether the existing code has a full path or separate components
Context: Files created by Claude Code hooks were visible inside the VM but not from the SSH terminal
Lesson: If a directory is recreated (e.g. by auto-prune), an SSH shell holding the old directory inode will not see new files — ls returns no such file even though cat with the full path works from other shells
Application: After ctx system prune or any state directory recreation, SSH sessions need cd-dot or re-login to pick up the new inode
Context: ctx system stats showed only old sessions, hiding the current one
Lesson: RFC3339 string comparison breaks when entries mix UTC (Z) and offset (-08:00) formats — 13:00-08:00 sorts before 18:00Z lexicographically despite being later in absolute time
Application: Always parse to time.Time before comparing RFC3339 timestamps; never rely on lexicographic sort
Context: context-mode proves both hooks work in production across 5 platforms
Lesson: ctx's hook architecture only uses UserPromptSubmit, PreToolUse, and PostToolUse — two lifecycle events are untapped
Application: PreCompact snapshot plus SessionStart re-injection would eliminate post-compaction disorientation without any new persistence layer since ctx agent already generates the content
Context: Found err.go files in 5 CLI packages with heavily duplicated error constructors (errFileWrite, errMkdir, errZensicalNotFound repeated across packages)
Lesson: Centralizing errors in internal/err eliminates duplication and prevents agents from continuing the pattern of adding local err.go files when they see one exists
Application: New error constructors go to internal/err/errors.go. No err.go files in CLI packages.
Context: Found 234 files in .context/state/ from weeks of sessions with no cleanup mechanism
Lesson: Session tombstones are write-only. Without auto-prune, the state directory grows unbounded. Added autoPrune(7) to context-load-gate so cleanup happens once per session at startup.
Application: Auto-prune is now wired into session start via context-load-gate. Manual prune still available via ctx system prune for aggressive cleanup.
Context: Memory drift nudge used memory-drift-nudged with no session ID in filename
Lesson: Any tombstone file intended to be session-scoped must include the session ID in its filename, otherwise it suppresses across all concurrent and future sessions. Use the UUID pattern so prune can clean them up.
Application: Audit all tombstone files for session-scoping; fixed memory-drift, but backup-reminded, ceremony-reminded, check-knowledge, journal-reminded, version-checked, ctx-wrapped-up still have this bug
Context: Filesystem and behavioral analysis of Claude Code v2.1.69
Lesson: Claude Code has two separate memory systems behind feature flags. Auto memory writes MEMORY.md to disk (user-visible, toggleable via settings). Session memory is a separate background extraction pipeline with compaction and team sync (push/pull model). The two systems serve different purposes and are independently feature-flagged.
Application: ctx memory bridge targets auto memory (MEMORY.md on disk). Session memory is API-side and not directly accessible. Full findings in ideas/claude-code-project-directory-structure.md.
Context: Draft of Agent Memory Is Infrastructure was publication-quality on first pass; user editorial feedback (structural emphasis, rhetorical sharpening, amnesia/archaeology bridge) elevated it significantly more than initial generation
Lesson: For narrative content, the first draft captures the argument; the editorial pass captures the voice. Both are necessary but the editorial pass has disproportionate impact on quality.
Application: For future blog posts, invest more in the editorial cycle (structural feedback then targeted refinements) rather than trying to nail voice on first generation.
Context: After make build, ran ./ctx deps --help which was blocked by block-non-path-ctx. Instead of asking user to install, tried cp ctx ~/bin/ — escalating workarounds.
Lesson: When a hook blocks an action, the correct response is to follow the hook's instruction (ask the user to sudo make install), not to find creative bypasses.
Application: Always ask the user to install when testing a freshly built binary. Never attempt alternative install paths to circumvent a hook.
Context: Adding billing.txt to embedded assets without a registry entry caused TestRegistryCoversAllEmbeddedFiles to fail immediately
Lesson: Every new .txt file under internal/assets/hooks/messages/ must have a corresponding entry in registry.go — the test acts as an exhaustive bidirectional check
Application: When adding new hook message variants, update the registry entry before running tests
Context: A doc admonition said Existing Projects: if you have an older key at X, it auto-migrates. Every project is existing once installed — the framing does not tell you how far behind you need to be.
Lesson: Version-anchored framing (Key Folder Change v0.7.0+) is clearer than relative framing (Existing Projects, Legacy). State the version boundary and the concrete action.
Application: When writing migration notes, anchor to a version number and give copy-pasteable commands, not vague auto-handled assurances.
Context: Heartbeat hook was reporting 16% usage at 162k tokens because it assumed claude-opus-4-6 always has 1M context window
Lesson: The JSONL model field is identical for both variants (both report claude-opus-4-6). The 1M context requires a beta header, not a different model ID. The user's model selection is stored in ~/.claude/settings.json with a [1m] suffix when 1M is active.
Application: Auto-detect context window from ~/.claude/settings.json model field containing [1m]. Default to 200k for all Claude models. The .ctxrc context_window setting is a no-op for Claude Code users.
Context: New tests used os.WriteFile(..., 0o644) for temp context files; lint flagged all three occurrences
Lesson: Gosec enforces 0600 max on WriteFile even in test code. Use 0o600 for test temp files
Application: Default to 0o600 for os.WriteFile in tests; only use wider permissions when testing permission behavior specifically
Context: Boundary violation test used subprocess pattern because original code called os.Exit(1)
Lesson: With PersistentPreRunE, errors propagate through Cobra Execute() return — no os.Exit call. Subprocess-based tests that expected exit codes need converting to direct error assertions
Application: When converting PreRun to PreRunE in Cobra commands, audit all tests that relied on os.Exit behavior
Context: After adding ~/.ctx/.ctx.key as global key location, test suites wrote real files to the developer home directory
Lesson: Any code that uses os.UserHomeDir() needs t.Setenv(HOME, tmpDir) in tests — especially test helpers called by many tests (like setupEncrypted and helper)
Application: When adding features that write to user-level paths (~/.ctx/, ~/.config/), always add HOME isolation to test setup functions first
[2026-03-01-133014] Task descriptions can be stale in reverse — implementation done but task not marked complete
Context: ctx recall sync task said 'command is not registered in Cobra' but the code was fully wired and all tests passed. The task description was stale.
Lesson: Tasks can become stale in the opposite direction from docs: implementation gets completed but the task is not updated. Always verify with ctx --help before assuming work remains.
Application: Before starting implementation on a 'code exists but not wired' task, run the command first to check if it already works.
Context: Implementing modelContextWindow() for the three-tier context window fallback. Claude model IDs use nested prefixes (claude-sonnet-4-5 vs claude-sonnet-4-20250514).
Lesson: A switch with ordered HasPrefix cases (most specific first) is cleaner and safer than iterating separate prefix lists. The catch-all 'claude-*' returns 200k for unrecognized Claude models.
Application: When adding new model families to modelContextWindow() in session_tokens.go, add the most specific prefix first to avoid shadowing shorter prefixes.
[2026-03-01-095709] TASKS.md template checkbox syntax inside HTML comments is parsed by RegExTaskMultiline
Context: Template had example checkboxes (- [x], - [ ]) in HTML comments that the line-based regex matched as real tasks, causing TestArchiveCommand_NoCompletedTasks to fail
Lesson: RegExTaskMultiline is line-based and has no awareness of HTML comment blocks — checkbox-like patterns inside comments get counted as real tasks
Application: Use backtick-quoted or indented references instead of actual checkbox syntax in template comments. When adding examples to TASKS.md templates, avoid patterns that match regExTaskPattern
Context: Investigated .context/logs/ and .context/state/ file management
Lesson: eventlog already rotates at 1MB with one previous generation. logMessage() in state.go was pure append-only with no size check.
Application: When adding new log sinks, follow the established rotation pattern (size-based, single previous generation)
[2026-02-28-184758] ctx pad import, ctx pad export, and ctx system resources make three hack scripts redundant
Context: Audited hack/ scripts against ctx CLI surface
Lesson: As ctx CLI grew, several hack scripts became wrappers around built-in commands (pad-import.sh -> ctx pad import, pad-export-blobs.sh -> ctx pad export, resource-watch.sh -> watch -n5 ctx system resources)
Application: Periodically audit hack/ for scripts that ctx has absorbed
Context: The installation section opened with 'A full ctx installation has two parts' — binary + Claude Code plugin — leaving non-Claude-Code users without a clear path
Lesson: Installation docs should lead with the universal requirement (the binary) and present agent-specific integration as conditional
Application: When writing docs for multi-tool projects, frame the common denominator first, then branch by tool
Context: hack/plugin-reload.sh was deleting ~/.claude/plugins/cache/activememory-ctx/ without repopulating it. Claude Code's installed_plugins.json still referenced the cache path, so the plugin appeared enabled but hooks.json was missing — all plugin hooks silently stopped firing.
Lesson: Claude Code snapshots plugin hooks from the cache directory at session startup. If the cache is deleted, plugin hooks vanish silently with no error. The reload script must rebuild the cache from source assets (internal/assets/claude/) after clearing it, and warn that a session restart is required.
Application: Always rebuild the plugin cache in hack/plugin-reload.sh. When debugging hooks that don't fire, check ~/.claude/plugins/cache/ first — a missing hooks.json is the most likely cause.
Context: The site/ directory contains generated HTML served directly from the repo (no CI build step). Multiple sessions have committed docs/ changes without the corresponding site/ output, or ignored site/ as 'generated noise'.
Lesson: site/ is intentionally tracked in git — there is no GitHub Pages workflow or CI step to build it. When docs change, the regenerated site/ HTML must be staged and committed alongside the source.
Application: Always git add site/ when committing changes under docs/. Never gitignore site/.
Context: ctx doctor reported context size against token_budget (8k) instead of context_window (200k), making 22k tokens look alarming.
Lesson: token_budget (ctx agent output trim target) and context_window (model capacity) serve different purposes. Health checks about context fitting should use context_window, with warning threshold proportional (e.g., 20% of window).
Application: Doctor now uses rc.ContextWindow() with 20% threshold and shows per-file token breakdown for actionable insight into which files are heavy.
Context: ctx drift flagged 23 warnings for backtick-quoted paths in CONVENTIONS.md and ARCHITECTURE.md that were prose examples (loader.go, session/run.go, sync.Once), not real file references.
Lesson: Path reference detection should verify the top-level directory exists on disk before flagging. Bare filenames and paths under non-existent directories are almost always examples in documentation.
Application: The fix checks os.Stat(topDir) on the first path component. Future drift checks on documentation-heavy files should use the same heuristic.
Consolidated from: 3 entries (2026-02-26)
- Verbal summaries with linked diagram files cut ARCHITECTURE.md from ~12K to ~3.8K tokens. Extract diagrams to linked files outside FileReadOrder; keep prose summaries inline. The 4-chars-per-token estimator is accurate — optimize content, not the estimator.
- Soft instructions have a ~75-85% compliance ceiling because "don't apply
judgment" is itself evaluated by judgment. When 100% compliance is required,
don't instruct — inject via
additionalContext. Reserve soft instructions for ~80% acceptable compliance. - Once ~7K tokens are auto-injected (fait accompli), the agent's rationalization inverts from "skip to save effort" to "marginal cost is trivial." Front-load highest-value content as injection, then use sunk cost to motivate on-demand reads for the remainder.
[2026-02-26-003854] Webhook silence after ctxrc profile swap is the most common notify debugging red herring
Context: Spent time investigating why webhooks weren't firing — checked binary version, hook configs, notify.Send internals. Actual cause was .ctxrc swapped to prod profile (notify commented out) earlier in session.
Lesson: When webhooks stop, check .ctxrc profile first (ctx config status). Also: not all tool uses trigger webhook-sending hooks — Read only
triggers context-load-gate (one-shot) and ctx agent (no webhook). qa-reminder
requires Edit matcher.
Application: Before debugging notify internals, run ctx config status and
verify the event would actually match a hook with notify.Send.
Consolidated from: 6 entries (2026-01-29 to 2026-02-24)
- CLI reference docs can outpace implementation: ctx remind had no CLI, ctx
recall sync had no Cobra wiring, key file naming diverged between docs and
code. Always verify with
ctx <cmd> --helpbefore releasing docs. - Structural doc sections (project layouts, command tables, skill counts) drift
silently. Add
<!-- drift-check: <shell command> -->markers above any section that mirrors codebase structure. - Agent sweeps for style violations are unreliable (8 found vs 48+ actual). Always follow agent results with targeted grep and manual classification.
- ARCHITECTURE.md missed 4 core packages and 4 CLI commands. The /ctx-drift skill catches stale paths but not missing entries — run /ctx-architecture after adding new packages or commands.
- Documentation audits must compare against known-good examples and pattern-match for the COMPLETE standard, not just presence of any comment.
- Dead link checking belongs in /consolidate's check list (check 12), not as a standalone concern. When a new audit concern emerges, check if it fits an existing audit skill first.
Consolidated from: 5 entries (2026-01-20 to 2026-01-25)
ctx agentis optimized for task execution (filters pending tasks, surfaces constitution, token-budget aware). Manual file reading is better for exploratory/memory questions (session history, timestamps, completed tasks).- On "Do you remember?" questions, immediately read .context/ files and run
ctx journal source --limit 5. Never ask "would you like me to check?" — that is the obvious intent. - .context/ is NOT a Claude Code primitive. Only CLAUDE.md and .claude/settings.json are auto-loaded. The .context/ directory requires a hook or explicit CLAUDE.md instruction to be discovered.
Orchestrator (IMPLEMENTATION_PLAN.md) and agent (.context/TASKS.md) task lists must be separate.(Superseded 2026-03-25: IMPLEMENTATION_PLAN.md removed. TASKS.md is the single task source.)- Only CLAUDE.md is auto-loaded by Claude Code. Projects using ctx should rely on the CLAUDE.md -> AGENT_PLAYBOOK.md chain, not AGENTS.md.
Consolidated from: 7 entries (2026-01-19 to 2026-02-26)
- Compiler-driven refactoring misses test files:
go build ./...catches production callsite breaks but not test files. Always rungo test ./...after signature changes. - All runCmd() returns must be consumed in tests: even setup calls need
_, _ = runCmd(...)to satisfy errcheck. - Set
color.NoColor = truein a package-level init function to disable ANSI codes for CLI test string assertions. - Recall CLI tests isolate via HOME env var:
t.Setenv("HOME", tmpDir)with.claude/projects/structure gives full isolation from real session data. formatDurationaccepts an interface with a Minutes method, not time.Duration directly. Use a stubDuration struct for testing.- CI tests need
CTX_SKIP_PATH_CHECK=1env var because init checks if ctx is in PATH. - CGO must be disabled for ARM64 Linux (
CGO_ENABLED=0) — CGO causes cross-compilation issues with-m64flag.
Consolidated from: 3 entries (2026-01-21 to 2026-02-17)
- Always use
ctxfrom PATH, never./dist/ctx-linux-arm64orgo run ./cmd/ctx. Checkwhich ctxif unsure. - Hooks must use PATH, not hardcoded paths.
ctx initchecks if ctx is in PATH before proceeding. Tests can skip withCTX_SKIP_PATH_CHECK=1. - Agent must never place binaries in any bin directory (not via cp, mv, or go
install). Build with
make build, then ask the user to run the privileged install step. Hooks in block-dangerous-commands.sh enforce this.
Consolidated from: 4 entries (2026-01-21 to 2026-02-17)
- Specs get lost without cross-references from TASKS.md. Three-layer defense: (1) playbook instruction, (2) spec reference in Phase header, (3) bold breadcrumb in first task.
- Subtask completion is implementation progress, not delivery. Parent tasks should have explicit deliverables; don't close until deliverable is verified.
- Exit criteria must include verification: integration tests (binary executes correctly), coverage targets, and smoke tests. "All tasks checked off" does not equal "implementation works."
- Reports graduate to ideas/done/ only after all items are tracked or resolved. Cross-reference every item against TASKS.md and the codebase before moving.
Consolidated from: 5 entries (2026-01-25 to 2026-02-22)
- Interaction pattern capture risks softening agent rigor. Do not build implicit user-modeling from session history. Rely on explicit, human-reviewed context (learnings, conventions, hooks) for behavioral shaping.
- Chain-of-thought prompting improves agent reasoning accuracy (17.7% to 78.7%). Added "Reason Before Acting" to AGENT_PLAYBOOK.md and reasoning nudges to 7 skills.
- Say "project conventions" not "idiomatic X" to ensure Claude looks at project files first rather than triggering training priors (stdlib conventions).
- Autonomous "YOLO mode" is effective for feature velocity but accumulates technical debt (magic strings, monolithic tests, hardcoded paths). Schedule periodic consolidation sessions.
- Trust the binary output over source code analysis. A single ambiguous CLI output is not proof of absence — re-run the exact command before claiming something is missing.
Consolidated from: 3 entries (2026-02-22 to 2026-02-25)
- Plain-text hook output is silently ignored by the agent. Claude Code parses
hook stdout starting with
{as JSON directives; plain text is disposable. All hooks should return JSON viaprintHookContext(). - Hook compliance degrades on narrow mid-session tasks (~15-25% partial skip rate). Root cause: CLAUDE.md's "may or may not be relevant" system reminder competes with hook authority. Fix: CLAUDE.md explicitly elevates hook authority. The mandatory checkpoint relay block is the compliance canary.
- No reliable agent-side before-session-end event exists. SessionEnd fires after the agent is gone. Mid-session nudges and explicit /ctx-wrap-up are the only reliable persistence mechanisms.
Consolidated from: 4 entries (2026-01-27 to 2026-02-14)
ctx add learningrequires--context,--lesson,--applicationflags.ctx add decisionrequires--context,--rationale,--consequence. A bare string only sets the title and the command will fail without required flags.- Structured entries with Context/Lesson/Application are more useful than one-liners. Agents are guided via AGENT_PLAYBOOK.md.
- Always complete decision record sections — placeholder text like "[Add context here]" is a code smell. Decisions without rationale lose their value over time.
- Slash commands using
!bash syntax require matching permissions in settings.local.json. When adding new /ctx-* commands, ensure ctx init pre-seeds the requiredBash(ctx <subcommand>:*)permissions.
Context: Discussed whether ctx should read and cache LEARNINGS.md, DECISIONS.md etc. in memory
Lesson: ctx is a short-lived CLI process, not a daemon. Context files are tiny (few KB), sub-millisecond to read. Cache invalidation complexity exceeds the read cost. Caching only makes sense if ctx becomes a long-lived process (MCP server, watch daemon).
Application: Don't add caching layers to ctx's file reads. If an MCP server mode is ever added, revisit then.
Consolidated from: 8 entries (2026-01-25 to 2026-02-17)
- Hook scripts receive JSON via stdin (not env vars); parse with
HOOK_INPUT=$(cat)then jq - Hook key names are case-sensitive:
PreToolUseandSessionEnd(notPreToolUseHooks) - Use
$CLAUDE_PROJECT_DIRin hook paths, never hardcode absolute paths - Hook regex can overfit:
ctxas binary vs directory name differ; anchor patterns to command-start positions with(^|;|&&|\|\|)\s* - grep patterns match inside quoted arguments — test with
ctx add learning "...blocked words..."to verify no false positives - Hook scripts can silently lose execute permission; verify with
ls -la .claude/hooks/*.shafter edits - Two-tier output is sufficient: unprefixed (agent context, may or may not
relay) and
IMPORTANT: Relay VERBATIM(guaranteed relay); don't add new severity prefixes - Repeated injection causes agent repetition fatigue; use
--session $PPID --cooldown 10mand pair with a readback instruction
Consolidated from: 2 entries (2026-02-12)
- UserPromptSubmit hook stdout is prepended as AI context (not shown to user); stderr with exit 0 is swallowed entirely
- User-visible output requires
{"systemMessage": "..."}JSON on stdout (warning banner) or exit 2 (blocks prompt) - There is no non-blocking user-visible output channel for this hook type
- Design hooks for their actual audience: AI-facing = plain stdout, user-facing = systemMessage JSON
Consolidated from: 7 entries (2026-01-25 to 2026-02-20)
- Full pre-commit gate: (1)
CGO_ENABLED=0 go build ./cmd/ctx, (2)golangci-lint run, (3)CGO_ENABLED=0 go test— all three, every time - Own the codebase: fix pre-existing lint issues even if you didn't introduce them
- gosec G301/G306: use 0o750 for dirs, 0o600 for files everywhere including tests
- gosec G304 (file inclusion): safe to suppress with
//nolint:gosecin test files usingt.TempDir()paths - golangci-lint errcheck: use
cmd.Printf/cmd.Printlnin Cobra commands instead offmt.Fprintf defer os.Chdir(x)fails errcheck; usedefer func() { _ = os.Chdir(x) }()- golangci-lint Go version mismatch in CI: use
install-mode: goinstallto build linter from source
Consolidated from: 4 entries (2026-02-15)
- Permission drift is distinct from code drift — settings.local.json is gitignored, no review catches stale entries
Skill()permissions don't support name prefix globs — list each skill individually- Wildcard trusted binaries (
Bash(ctx:*),Bash(make:*)), but keep git commands granular (neverBash(git:*)) - settings.local.json accumulates session debris; run periodic hygiene via
/sanitize-permissionsand/ctx-drift
Consolidated from: 3 entries (2026-02-11 to 2026-02-15)
- Gitignored directories are invisible to
git status; stale artifacts persist indefinitely — periodicallylsgitignored working directories - Add editor artifacts (*.swp, *.swo, *~) to .gitignore alongside IDE directories from day one
- Gitignore entries for sensitive paths are security controls, not documentation — never remove during cleanup sweeps
Context: Considering whether to build custom UI for .context/ files
Lesson: Discovery, search, and editing of .context/ markdown files works better in VS Code/IDE than any custom UI we'd build. Full-text search, git integration, extensions - all free.
Application: Don't reinvent the editor. Let users use their preferred IDE.
Module-specific, niche, and historical learnings: learnings-reference.md