Click a file path under a Read, Edit or Write row to show that file's contents in a pane beside the transcript.

ushironoko's dotfiles management system.
# Initial setup
git clone https://github.com/ushironoko/dotfiles.git
cd dotfiles
./init.sh
# After setup, the dotfiles command is available:
dotfiles install # Install symlinks
dotfiles list # List managed files
dotfiles restore # Restore from backup
dotfiles doctor # Check environment
# Future updates with ghq (after initial setup):
ghq get ushironoko/dotfiles
cd $(ghq root)/github.com/ushironoko/dotfiles
dotfiles installCreates symbolic links from repository to system locations.
-d, --dryRun: Preview changes-f, --force: Force overwrite-s, --select: Interactive selection-v, --verbose: Detailed outputdotfiles listShows all managed dotfiles and their status.
-v, --verbose: Detailed informationdotfiles restoreRestores files from backup.
-b, --backup <timestamp>: Specific backup-l, --list: List available backups-v, --verbose: Detailed outputdotfiles doctorDiagnoses environment issues.
-c, --check <categories>: Check specific areas (environment,conflicts,ghq,config,mcp)-v, --verbose: Detailed diagnosticsIf you're having issues with the initial setup, run the diagnostic tool:
./init.sh --check
This will show:
Run dotfiles doctor to diagnose issues. Common fixes:
# If mise commands are not found after installation
# Make sure ~/.local/bin is in your PATH and restart shell
export PATH="$HOME/.local/bin:$PATH"
exec $SHELL
# Remove conflicting Bun installation
rm -rf ~/.bun
# Fix PATH permanently
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
exec $SHELL
# Migrate old repos to ghq
./scripts/migrate-to-ghq.sh --dry-run
./scripts/migrate-to-ghq.sh --symlink
Edit dotfiles.config.ts to manage your files:
The versioned Codex harness installs global instructions, native custom agents, lifecycle hooks, command restrictions, and the shared Claude skills alongside ~/.codex/config.toml. Each component is linked selectively so Codex runtime state and bundled skills remain intact. After hook changes, review and trust them with /hooks in Codex.
~/.codex/config.toml)~/.codex/config.toml is symlinked to codex/config.toml, but codex rewrites that file at runtime, filling it with machine-local state — [projects."<path>"] trust levels (absolute paths, including private/client repo names), [mcp_servers.*] tables wired to Codex.app paths, [marketplaces.*] sources, the notify helper path, per-repo [desktop...perPath] prefs, and [hooks.state] approval hashes. A git clean filter (codex-scrub, codex/scrub-config.awk) drops anything carrying a quoted absolute path, every [projects.*]/[mcp_servers.*] table, and the complete [hooks.state] tree at git add time. The path rule remains content-based so unknown machine state is scrubbed by default; URLs and relative paths are kept. The working tree keeps the live file untouched, so codex keeps functioning.
The filter driver lives in .git/config (never committed), so it must be registered once per clone:
bun run setup:git-filters # also run automatically by init.sh and run-all
init.sh runs it on initial setup and bun run run-all runs it before every pre-commit check, so under normal use no manual step is needed. If you commit codex/config.toml on a fresh clone without it, trust state would leak — the filter is set to required = true to fail loudly if the scrubber ever errors.
hooks/register.tsx 192 lines1import type { ElementTable, On } from "claude-code";
2
3const PANE_ID = "file-view";
4const MAX_SOURCE_CHARS = 10000;
5const FILE_TOOLS: ReadonlySet<string> = new Set([
6 "Read",
7 "Edit",
8 "Write",
9 "MultiEdit",
10 "NotebookEdit",
11]);
12
13type Viewed =
14 | { kind: "loading"; path: string }
15 | { kind: "text"; path: string; source: string; isTruncated: boolean }
16 | { kind: "error"; path: string; message: string };
17
18interface Engine {
19 read: (path: string) => Promise<string>;
20 open: (title: string) => Promise<unknown>;
21 redraw: () => void;
22}
23
24type BodyKit = Pick<ElementTable<"terminal">, "Box" | "Text" | "Code">;
25
26const filePathOf = (input: unknown): string | null => {
27 if (typeof input !== "object" || input === null) {
28 return null;
29 }
30
31 const record = input as Record<string, unknown>;
32 const path = record.file_path ?? record.notebook_path;
33
34 return typeof path === "string" && path !== "" ? path : null;
35};
36
37const basenameOf = (path: string): string => {
38 const trimmed = path.endsWith("/") ? path.slice(0, -1) : path;
39
40 return trimmed.slice(trimmed.lastIndexOf("/") + 1) || path;
41};
42
43const isDrawable = (char: string): boolean => {
44 const code = char.codePointAt(0) ?? 0;
45
46 return code === 9 || code === 10 || (code >= 32 && code !== 127);
47};
48
49const drawableOf = (text: string): string =>
50 [...text.replaceAll("\r\n", "\n")].filter(isDrawable).join("");
51
52const boundedOf = (text: string): { source: string; isTruncated: boolean } => {
53 if (text.length <= MAX_SOURCE_CHARS) {
54 return { source: text, isTruncated: false };
55 }
56
57 const cut = text.lastIndexOf("\n", MAX_SOURCE_CHARS);
58
59 return {
60 source: text.slice(0, cut > 0 ? cut : MAX_SOURCE_CHARS - 1),
61 isTruncated: true,
62 };
63};
64
65const messageOf = (error: unknown): string =>
66 error instanceof Error ? error.message : String(error);
67
68const loadedOf = async (engine: Engine, path: string): Promise<Viewed> => {
69 try {
70 const raw = await engine.read(path);
71
72 if (raw.includes("\u0000")) {
73 return { kind: "error", path, message: "Binary file" };
74 }
75
76 return {
77 kind: "text",
78 path,
79 ...boundedOf(drawableOf(raw.slice(0, MAX_SOURCE_CHARS * 2))),
80 };
81 } catch (error) {
82 return { kind: "error", path, message: messageOf(error) };
83 }
84};
85
86const bodyOf = ({ Box, Text, Code }: BodyKit, shown: Viewed | null) => {
87 if (shown === null) {
88 return (
89 <Text dimColor>Click a file path under a tool row to show it here.</Text>
90 );
91 }
92
93 switch (shown.kind) {
94 case "loading": {
95 return <Text dimColor>Loading…</Text>;
96 }
97 case "error": {
98 return <Text color="red">{shown.message}</Text>;
99 }
100 case "text": {
101 return (
102 <Box flexDirection="column">
103 <Code source={shown.source} path={shown.path} startLine={1} />
104 {shown.isTruncated ? (
105 <Text dimColor>… truncated at {MAX_SOURCE_CHARS} characters</Text>
106 ) : null}
107 </Box>
108 );
109 }
110 }
111};
112
113export const register = (on: On) => {
114 let viewed: Viewed | null = null;
115 let generation = 0;
116
117 const show = async (engine: Engine, path: string) => {
118 generation += 1;
119 const current = generation;
120
121 viewed = { kind: "loading", path };
122 await engine.open(basenameOf(path));
123 engine.redraw();
124
125 const loaded = await loadedOf(engine, path);
126
127 if (current === generation) {
128 viewed = loaded;
129 engine.redraw();
130 }
131 };
132
133 on("ui.render", { component: "ToolUse" }, async ($, e, next) => {
134 const drawn = await next(e);
135 const path = FILE_TOOLS.has(e.props.tool)
136 ? filePathOf(e.props.input)
137 : null;
138
139 if (path === null || e.surface !== "terminal") {
140 return drawn;
141 }
142
143 const { Box, Button } = $.ui.resolve(e);
144
145 const engine: Engine = {
146 read: (file) => $.fs.read(file),
147 open: (title) => $.ui.open({ id: PANE_ID, title }),
148 redraw: () => $.ui.invalidate("ui.render"),
149 };
150
151 return (
152 <Box flexDirection="column">
153 {drawn}
154 <Box paddingLeft={2}>
155 <Button
156 key={`open:${e.requestId}`}
157 plain
158 dimColor
159 label={`⧉ ${path}`}
160 onPress={() => void show(engine, path).catch(() => undefined)}
161 />
162 </Box>
163 </Box>
164 );
165 });
166
167 on("ui.render", { component: "Pane" }, async ($, e, next) => {
168 if (e.requestId !== PANE_ID || e.surface !== "terminal") {
169 return next(e);
170 }
171
172 const { Box, Text, Code } = $.ui.resolve(e);
173 const shown = viewed;
174
175 return (
176 <Box flexDirection="column" paddingRight={1}>
177 <Text bold wrap="truncate-start">
178 {shown?.path ?? "file-view"}
179 </Text>
180 {bodyOf({ Box, Text, Code }, shown)}
181 </Box>
182 );
183 });
184
185 on("ui.close", { id: PANE_ID }, (_, e, next) => {
186 viewed = null;
187 generation += 1;
188
189 return next(e);
190 });
191};
192