SLOPSHOPPER

message-times

Shows when each prompt and reply in the transcript was stored, dim under its row.

newrows
★ 9v0.1.0no licenseupdated 2026-10-21berlysia/dotfiles/mods/message-times
A shopper browsing a rack in a slop shop
README

dotfiles

depends on chezmoi

How to use(for me)

  1. install chezmoi
  2. sh -c "$(curl -fsLS get.chezmoi.io)" -- -b $HOME/.local/bin
  3. winget install twpayne.chezmoi
  4. if you are in PowerShell, Set-ExecutionPolicy -ExecutionPolicy ByPass -Scope Process
  5. chezmoi init --apply berlysia

note

  • ~/.local/bin will be added to $PATH
  • ~/.local/.bin will be added to $PATH , and overwrite this directory with symlink

Tailscale経由のSSHと、公開リポジトリに端末情報を置かない鍵管理はSSH設定の手順を参照。

Local CI

Local workflow verification uses actrun, managed by this project's mise config.

mise install
pnpm ci:local:typescript
pnpm ci:local:codex
pnpm ci:local:shell

If your shell is not activated by mise, run commands through mise exec -- ... instead.

GitHub Environments

Claude workflows use environment-scoped secrets instead of repository-wide secrets.

Add CLAUDE_CODE_OAUTH_TOKEN to both environments. If you want unattended automation, leave approvals and wait timers disabled.

list of something will be set up

  • homebrew (only mac)
  • mise
  • direnv
  • fzf
  • ripgrep
  • bat
  • WSL2 ssh-agent (only WSL)
  • my custom zsh prompt

Claude Decision Log Analysis

This dotfiles project includes tools for analyzing Claude Code hook decision logs:

home/dot_claude/scripts/update-auto-approve.ts

Automated permission pattern analysis and management tool:

# Interactive review of all patterns
bun home/dot_claude/scripts/update-auto-approve.ts

# Preview changes without applying
bun home/dot_claude/scripts/update-auto-approve.ts --dry-run

# Analyze last 7 days only
bun home/dot_claude/scripts/update-auto-approve.ts --since 7d

# Auto-approve safe patterns without interaction
bun home/dot_claude/scripts/update-auto-approve.ts --auto-approve-safe

# Show detailed analysis information
bun home/dot_claude/scripts/update-auto-approve.ts --verbose

Features:

  • Analyzes ~/.claude/logs/decisions.jsonl for permission patterns
  • Provides risk scoring and confidence levels
  • Interactive review interface for pattern approval
  • Automatic backup of permission files
  • Supports time-based filtering of logs

Claude Code Configuration

This repository includes comprehensive Claude Code configuration with:

  • Custom skills (see .skills/ directory)
  • Global development guidelines (CLAUDE.md)
  • Development rules (debugging, external-review, TypeScript standards)

Settings.json Split Management

~/.claude/settings.json is managed by splitting it into multiple files:

  • .settings.base.json.tmpl: Core settings (model, language, statusLine, alwaysThinkingEnabled, etc.)
  • .settings.permissions.json: Permission settings (allow/deny)
  • .settings.hooks.json.tmpl: Hooks configuration (dynamically generated with chezmoi variables)

These files are automatically merged by run_onchange_update-settings-json.sh.tmpl during chezmoi apply.

Important notes:

  • To modify hooks configuration, edit .settings.hooks.json.tmpl and run chezmoi apply
  • enabledPlugins is managed by the claude plugin CLI; existing values are preserved on merge (see Plugin Management)

~/.claude.json Automatic Management

~/.claude.json (MCP servers configuration) is automatically managed by run_onchange_update-claude-json.sh.tmpl:

How it works:

  • Reads MCP server versions from package.json dependencies
  • Merges template content with existing ~/.claude.json
  • Auto-updates mcpServers, preferredNotifChannel, and defaultMode
  • Creates backup before changes and shows diff

The managed MCP servers are defined in the template of run_onchange_update-claude-json.sh.tmpl.

To update versions: Edit package.json dependencies and run chezmoi apply

Dual-Layer Skills Management

This project uses a two-layer approach for managing Claude Code skills:

1. Handcrafted Skills (.skills/)
  • Stored in ${projectRoot}/.skills/ for unified management (at repo root, outside home/)
  • Synced to both ~/.claude/skills/ and ~/.codex/skills/ via run_after_sync-skills.sh.tmpl
  • Used by both Claude Code and Codex
  • Always preserved during updates
2. External Skills (home/dot_apm/apm.yml)
  • Declaratively managed in APM manifest (apm.yml)
  • Installed via apm install -g from GitHub repositories
  • Tracked via apm.lock.yaml
  • Auto-removed when deleted from manifest (handcrafted skills are protected)

Both layers coexist in ~/.claude/skills/ directory.

Plugin Management

Plugins and marketplaces are declared in home/.chezmoidata/claude_plugins.yaml. chezmoi apply installs and removes them to match the declaration (run_onchange_install-claude-plugins-8.sh.tmpl).

Auditing and syncing:

# Compare declared plugins (YAML) with the actual state of the claude CLI
~/.claude/scripts/show-missing-plugins.sh

# Print YAML generated from the installed plugins and diff it against the declaration
~/.claude/scripts/sync-enabled-plugins.sh

Using in Other Projects

To use these skills in another project:

cd /path/to/your/project
curl -fsSL https://raw.githubusercontent.com/berlysia/dotfiles/master/scripts/setup-claude-skills.sh | bash

This creates a simple .claude/settings.json with inline SessionStart command - no hook files needed!

Skills are installed to ~/.claude/ and auto-update on Claude Code startup.

See docs/external-usage.md for detailed documentation.

Skills Included

See .skills/ directory for available skills.

Source 2 files
hooks/register.tsx 69 lines
1import { atom, memberOf, read, update } from "claude-code";
2import type {
3  EngineInterface,
4  Register,
5  RenderElement,
6  RenderInput,
7} from "claude-code";
8
9// A family keyed by the stored row's uuid, so a write redraws that row alone.
10const storedAt = atom(
11  { plugin: "message-times", key: "storedAt" } as const,
12  null,
13);
14
15export function formatDateTime(ms: number): string {
16  const date = new Date(ms);
17  const pad = (n: number) => String(n).padStart(2, "0");
18  const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
19  const time = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
20  // An offset, not a zone name like JST: a name needs Intl's time zone data,
21  // which the mod's runtime is not promised to carry.
22  const offsetMinutes = -date.getTimezoneOffset();
23  const sign = offsetMinutes < 0 ? "-" : "+";
24  const abs = Math.abs(offsetMinutes);
25  const offset = `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
26  return `${day} ${time} ${offset}`;
27}
28
29async function drawWithTime(
30  $: EngineInterface,
31  e: RenderInput<"UserMessage" | "AssistantMessage">,
32  beneath: RenderElement,
33): Promise<RenderElement> {
34  const at = await read($, memberOf(storedAt, e));
35  // A row stored before the mod loaded (a resumed session's history) has no
36  // time here; the transcript file has one, which this mod does not read.
37  if (at === null) return beneath;
38  const { Box, Text } = $.ui.resolve(e);
39  return (
40    <Box flexDirection="column">
41      {beneath}
42      <Text dimColor>{`  ${formatDateTime(at)}`}</Text>
43    </Box>
44  );
45}
46
47export const register: Register = (on) => {
48  on("session.append", async ($, e, next) => {
49    const result = await next(e);
50    if (result.deny !== undefined) return result;
51    const { type } = result.message;
52    if (type === "user" || type === "assistant") {
53      const at = await $.clock.now();
54      // A message row's requestId is the uuid its row was stored under.
55      await update($, memberOf(storedAt, { requestId: result.uuid }), () => at);
56    }
57    return result;
58  });
59
60  on("ui.render", { component: "UserMessage" }, async ($, e, next) =>
61    drawWithTime($, e, await next(e)),
62  );
63  // Every block of a reply: the blocks either side of a tool call were
64  // written at different times.
65  on("ui.render", { component: "AssistantMessage" }, async ($, e, next) =>
66    drawWithTime($, e, await next(e)),
67  );
68};
69
types/index.d.ts 13 lines
1declare module "claude-code" {
2  interface PluginState {
3    "message-times": {
4      /**
5       * When `session.append` stored each user or assistant row, in
6       * `$.clock.now()`'s milliseconds, by the row's uuid (a message row's
7       * `requestId`); null for a row stored before the mod loaded.
8       */
9      storedAt: StateFamily<number | null>;
10    };
11  }
12}
13