Configuration
This page covers Ghostwriter's settings screen, config/ghostwriter.php, the plugin's options, where Ghostwriter keeps things, prompts and logging.
Settings
Ghostwriter → Settings, for people who manage Ghostwriter. On a panel with tenancy, nobody does until you say who.

| Section | Setting | Key | Default |
|---|---|---|---|
| Writing | Provider | provider |
anthropic; or openai, gemini |
| Writing | Model | model |
blank: the provider's default (API keys) |
| Images | Image provider | image_provider |
blank: whichever has a key; or openai, gemini |
| Images | Image model | image_model |
blank: the provider's default |
| Images | Search Openverse | openverse |
on. Free CC0 and public-domain photos |
| Images | Mark images still to choose | placeholder_images |
on. Placeholders in empty image fields |
| Overview | Suggest kinds of content automatically | suggest_kinds_automatically |
on. In Get started only |
| Overview | Show Get started | show_get_started |
on. See Get started |
The API keys section lists each key as "set" or "not set". It never shows the keys themselves.
If the Model looks like another provider's (a gpt-… model with Claude chosen, say), saving warns you. It's saved all the same, since a provider's new models may not follow the usual names.
Settings are stored in the ghostwriter_settings table and apply to the whole app, across every panel and tenant.
Settings fixed in config
A setting under settings in config/ghostwriter.php wins over the settings screen, on every environment. The field is shown locked, with the note "Set in config/ghostwriter.php.", and can't be changed there.

'settings' => [
'provider' => env('GHOSTWRITER_PROVIDER', 'anthropic'),
'suggest_kinds_automatically' => false,
'timeout' => 600,
],
The time limit
How long to wait for one answer, per attempt, is set only in config/ghostwriter.php, as timeout under settings. The default is 300 seconds. It isn't on the settings screen, because the queue worker's --timeout has to allow for it (see The queue).
config/ghostwriter.php
Published by ghostwriter:install. Besides settings, it holds:
// Keys for writing and making images. A key left blank here falls back
// to the Laravel AI SDK's config/ai.php, if the app has one.
'keys' => [
'anthropic' => env('ANTHROPIC_API_KEY'),
'openai' => env('OPENAI_API_KEY'),
'gemini' => env('GEMINI_API_KEY'),
],
// A gateway that speaks the provider's own API, in place of the
// provider's address. Null uses the provider.
'base_urls' => [
'anthropic' => env('GHOSTWRITER_ANTHROPIC_BASE_URL'),
'openai' => env('GHOSTWRITER_OPENAI_BASE_URL'),
'gemini' => env('GHOSTWRITER_GEMINI_BASE_URL'),
],
// When Claude declines a request, answer it with Anthropic's recommended
// fallback model. Only on Anthropic's own API.
'anthropic_fallbacks' => true,
// The log channel for model calls, retries and failures. Null uses the
// default. Prompts and keys are never logged; replies only as below.
'log_channel' => env('GHOSTWRITER_LOG_CHANNEL'),
'debug' => [
// Put the whole reply in the log when a model's reply can't be read.
// Off by default. See Logging, below.
'log_replies' => (bool) env('GHOSTWRITER_LOG_REPLIES', false),
],
// Keys for the photo libraries. Optional.
'photo_libraries' => [
'unsplash' => env('UNSPLASH_ACCESS_KEY'),
'pexels' => env('PEXELS_API_KEY'),
'pixabay' => env('PIXABAY_API_KEY'),
],
// Conversations shared with everyone who may use Ghostwriter (within the
// tenant), or false to keep each to whoever started it. See Permissions.
'shared_conversations' => env('GHOSTWRITER_SHARED_CONVERSATIONS', true),
// The queue model calls are sent to. Null uses the default.
'queue' => env('GHOSTWRITER_QUEUE'),
// Database tables are prefixed with this.
'table_prefix' => 'ghostwriter_',
Keys are read from the environment every time and never stored. See API keys, and Gateways and proxies for base_urls.
The plugin
Set per panel, where the plugin is registered:
| Method | What it does |
|---|---|
resources([...]) |
The resources Ghostwriter writes for, each optionally keyed to a closure narrowing it to published records |
canManage(fn (User $user): bool => …) |
Who manages Ghostwriter on this panel |
navigationGroup(?string) |
The group the Ghostwriter item sits in (none by default). With navigation(cluster: false), the group its pages sit in ('Ghostwriter' by default) |
navigationSort(?int) |
Where the Ghostwriter item sits in its group |
subNavigationPosition(SubNavigationPosition) |
Ghostwriter's pages as tabs across the top (Top, the default) or listed down the side (Start) |
navigation(cluster: bool) |
One Ghostwriter item with its pages inside (the default), or false for each page as its own item. See Navigation |
imageButton(bool) |
The image button on upload fields. On by default |
widget(bool) |
The dashboard widget. Off by default |
Where things are kept
Everything is in the database, in tables prefixed ghostwriter_ (table_prefix):
| Table | Holds |
|---|---|
ghostwriter_settings |
Settings saved on the settings screen |
ghostwriter_guides |
The voice guide and image style guide |
ghostwriter_kinds |
Kinds of content |
ghostwriter_ideas |
The content plan |
ghostwriter_sessions |
Pieces being written or edited: the brief, the conversation (with who sent each message), the draft, who started it and who last changed it |
ghostwriter_form_maps |
What was read of each resource's form |
ghostwriter_states |
Working state: suggestions waiting for review, jobs in progress, queued: marks for jobs not yet picked up, and photo requests (with the user_id of the person who made each) |
Every table except settings is kept per tenant on a panel with tenancy.
Files: images made but not yet used are kept on the local disk under ghostwriter/images, and cleared after a day. Chosen images go to the upload field's own disk and directory.
Overriding prompts
Every prompt is a markdown file in resources/prompts/ of the 1994/ghostwriter-core package, which Ghostwriter installs (vendor/1994/ghostwriter-core/resources/prompts/). To change one, copy it to resources/ghostwriter/prompts/ in your app with the same name and edit it there.
The shared prompts use [[...]] placeholders for the words that differ between Ghostwriter's apps: [[item]] becomes "record", [[group]] "resource", [[place]] "app", and so on. They are filled in your copy too, so you can keep them or write the words out.
| Prompt | Used for |
|---|---|
writer.md |
Writing and revising drafts |
brief-writer.md |
Filling in a brief from a working title and notes |
voice-analyst.md, voice-editor.md |
Writing and changing the voice guide |
type-analyst.md, kind-finder.md |
Learning and suggesting kinds |
planner.md |
The content plan |
imagery-analyst.md |
The image style guide |
photo-researcher.md, photo-picker.md |
Searching for and picking photos |
image.md |
Making images |
Keep the reply formats the prompts ask for (the tagged blocks and YAML), or Ghostwriter won't be able to read the answers. photo-picker answers one line per photo that fits, number: why, best first, or none: search; search; search when none does; a plain list of numbers, as older overrides asked for, is still read.
Logging
Ghostwriter logs to the log_channel in config/ghostwriter.php (GHOSTWRITER_LOG_CHANNEL), or the app's default channel. It logs:
- each retry of a model call, with the provider, the status and the wait
- each failed model call, with the provider and the error
- a reply cut off at its token limit, with the provider and model
- a failed photo search or ranking
- a reply that couldn't be read, with the agent and what was wrong with it: "the type analysis for posts could not be read (there was no
<type>block); asking again", or "the YAML did not parse at line 3"
Prompts and keys are never logged, and neither is what the model wrote: a reply holds your app's content.
To see a reply that couldn't be read, while tracking down a problem, set GHOSTWRITER_LOG_REPLIES=true (debug.log_replies). The whole reply is then added to that log entry's context, under reply, never to the message. Turn it off again afterwards. Restart the queue worker after changing it (php artisan queue:restart).